Skip to content

Sync/upstream main 2026 07 06#16

Merged
kaznaan merged 1146 commits into
mainfrom
sync/upstream-main-2026-07-06
Jul 6, 2026
Merged

Sync/upstream main 2026 07 06#16
kaznaan merged 1146 commits into
mainfrom
sync/upstream-main-2026-07-06

Conversation

@kaznaan

@kaznaan kaznaan commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

chore: sync main with upstream tldraw v5.2.3

kaneel and others added 30 commits May 28, 2026 15:46
…ks (tldraw#8952)

Emit rel=canonical on every article via alias→canonical mapping
(quick-start, installation, releases, examples, starter-kits). Filter
sitemap to canonical paths only. Point footer, category menu, and 404
nav at canonical hrefs instead of URLs that 301.

### Change type

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

Seems I went a bit too quickly with the previous work on docs and
canonical urls and actually removed the thing that makes the quick-start
page actually work

### Change type

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

## Summary

The latest few `tldraw@next` versions on npm ship with `"workspace:*"`
literals in their `package.json`, which breaks installs for any consumer
outside this monorepo:

```
$ yarn up tldraw@next
Error: @tldraw/driver@workspace:*: Workspace not found (@tldraw/driver@workspace:*)
```

Inspecting the broken tarball confirms the source `package.json`
(tab-indented, `workspace:*` deps) is shipped untouched:

```
$ npm pack [email protected]
$ tar -xzOf tldraw-5.1.0-next.d83788c14b6f.tgz package/package.json | grep '@tldraw/'
        "@tldraw/driver": "workspace:*",
        "@tldraw/editor": "workspace:*",
        "@tldraw/store":  "workspace:*",
```

Comparing to a prior good publish (`5.1.0-next.d7c83ba698ae`) shows the
expected output — 2-space-indented `package.json` with rewritten npm
versions. The registry metadata also tells the story:

| | Good (`d7c83ba698ae`) | Bad (`d83788c14b6f`) |
|---|---|---|
| `_npmUser` | `tldraw-personal <[email protected]>` | `GitHub Actions
<[email protected]>` |
| `_npmVersion` | (yarn) | `11.8.0` (npm CLI) |
| provenance | none | OIDC attestation |

## Root cause

PR tldraw#8913 swapped the publish call in
`internal/scripts/lib/publishing.ts` from `yarn npm publish` to `npm
publish`. The PR motivation — switching to npm's OIDC trusted-publisher
flow — was correct, but the swap had a silent side effect: `yarn npm
publish` rewrites `workspace:*` dependency specifiers into concrete
sibling versions during pack, and `npm publish` doesn't (the workspace
protocol is a yarn-specific extension that npm has no concept of).

The swap turns out to have been unnecessary. **Yarn 4.10**
([yarnpkg/berry#6898](yarnpkg/berry#6898), Oct
2025) added native OIDC trusted-publishing support. When `yarn npm
publish` runs in GitHub Actions with `permissions: id-token: write`
granted, yarn requests the GitHub-issued OIDC token, exchanges it with
the npm registry, and publishes — exactly like the npm CLI does.
Provenance attestations are produced automatically. This repo is already
on yarn 4.12 via the `packageManager` field, so OIDC was supported the
whole time tldraw#8913 was being written.

## Change

This PR reverts only the publish-call change from tldraw#8913 and updates the
surrounding comment to reflect the yarn-driven OIDC path. The
workflow-side changes from tldraw#8913 (`permissions: id-token: write` on each
publish job, removal of `NPM_TOKEN` env, trusted-publisher configuration
on npmjs.com) are unchanged and remain required.

`--tolerate-republish` is restored alongside the yarn invocation since
it's a yarn-specific flag; the substring-match catch block (added in
tldraw#8913 to compensate for npm CLI not having `--tolerate-republish`) is
kept as a belt-and-braces defense, because the original code comment
notes `--tolerate-republish` is unreliable for canary versions.

## Test plan

- [ ] Merge to `main` and watch `publish.yml` run `canary` mode
end-to-end on the next push to `main`.
- [ ] After publish, `npm pack <new-canary-version>` and confirm
dependency specifiers in the tarball's `package.json` show concrete
versions (e.g. `"@tldraw/editor": "5.1.0-canary.<sha>"`), not
`"workspace:*"`.
- [ ] Confirm the new canary version still shows a provenance
attestation badge on npmjs.com (yarn 4.10+ produces these automatically
under OIDC).
- [ ] In a consumer repo, run `yarn up tldraw@canary` and verify it
resolves without the "Workspace not found" error.

After this lands, the `5.1.0-next.*` versions published since tldraw#8913
merged are still broken on npm; the registry doesn't allow unpublishing
after 72 hours so they'll need to be left as-is, with the next good
`next` publish (post-merge) being the recommended upgrade target.
…review (tldraw#8964)

In order to stop the "Deploy .com to preview" workflow from failing on
the "Create Supabase preview branch" step, this PR makes the
branch-provisioning wait loop tolerant of the Supabase CLI's output
while a branch is still being created.

The Supabase CLI
[v2.102.0](https://github.com/supabase/cli/releases/tag/v2.102.0)
[ported the `branches` commands from Go to native
TypeScript](supabase/cli#5374). Because our
workflow installs the CLI with `version: latest`, CI picked this up
automatically. The rewrite changed what `supabase branches get -o json`
writes to **stdout** while a branch is still provisioning: the old (Go)
CLI wrote empty stdout there, but the new (TS) CLI renders a non-JSON
error/status line. The workflow piped that straight into an unguarded
`jq`, which exited with code 5 on the parse error, and since the step
runs under `set -e` the whole step aborted on the first loop iteration —
before the retry loop could wait for the branch to become ready.

This was a CLI behavior change, not a regression in our code: the script
had been unchanged since the [Supabase preview-DB
migration](tldraw#8066), and the same
workflow passed on older CLI versions.

The fix adds `2>/dev/null || true` to `supabase branches get` and to
both `jq` calls so that, while the branch is provisioning, non-JSON /
non-zero output is treated as "not ready yet" and the loop keeps
retrying as it was always meant to. Once the branch is ready the CLI
returns valid JSON, `jq` extracts `POSTGRES_URL`, and the loop breaks
normally. Field names (`POSTGRES_URL`, `POSTGRES_URL_NON_POOLING`) are
unchanged in the new CLI, so no other adjustments are needed, and the
CLI stays on `latest`.

### Change type

- [x] `other` (CI)

### Test plan

1. Open a PR that triggers the "Deploy .com to preview" workflow with a
brand-new preview branch (one that has to provision from scratch).
2. Confirm the "Create Supabase preview branch" step logs "Waiting for
branch to be ready (i/24)..." while provisioning and then succeeds,
instead of failing with `jq: parse error` / exit code 5.

### Code changes

| Section        | LOC change |
| -------------- | ---------- |
| Config/tooling | +8 / -3    |
…raw#8945)

In order to let pre-commit hooks run cleanly from inside git worktrees,
this PR bumps `oxlint` from 1.58.0 to 1.66.0.

### The bug

When a worktree lives under the main repo (for example
`.claude/worktrees/<name>/`), oxlint 1.58 walks up from the file being
linted, finds the worktree's `.oxlintrc.json`, and then keeps walking
and also finds the main repo's `.oxlintrc.json`. It treats one of the
two identical configs as a "nested" config and rejects
`options.typeAware`:

```
The `options.typeAware` option is only supported in the root config,
but it was found in /Users/.../.oxlintrc.json
```

The net effect: the lint-staged step in `.husky/pre-commit` blows up for
every commit made from inside a worktree, regardless of what the diff
is, which forces contributors (and Claude Code agents) to use `git
commit --no-verify`.

### The fix

Upstream fixed this in oxlint 1.64 —
[oxc-project/oxc#22272](oxc-project/oxc#22272)
("load root config by searching up parent directories"). Bumping to 1.66
(latest available in our registry mirror) picks up the fix at the
source. No config or script changes needed.

The earlier commit on this branch added a `--disable-nested-config`
workaround flag to `lint-staged` and `internal/scripts/lint.ts`; the
follow-up commit reverts it now that the bump makes it unnecessary.

### Change type

- [x] `other`

### Test plan

1. `git worktree add .claude/worktrees/test-worktree`
2. `cd .claude/worktrees/test-worktree && yarn install`
3. `yarn oxlint packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts`
— exits 0, no `options.typeAware` parse error.
4. Make any trivial change inside the worktree, `git add` it, and `git
commit` (without `--no-verify`) — pre-commit hook completes cleanly.

### Code changes

| Section        | LOC change   |
| -------------- | ------------ |
| Config/tooling | +85 / -85    |

(`package.json` +1 / -1, `yarn.lock` +84 / -84.)
Closes tldraw#8965

## What

Dotcom previews now always deploy a single-node Zero backend, so the
`@tldraw.com` "proper Zero" opt-in is valid on every preview.

Previously a plain `dotcom-preview-please` deploy set
`DEPLOY_ZERO=false` unless the `preview-flyio-zero-deploy-please` label
was present, but the client still opted any `@tldraw.com` user into
proper Zero — pointing staff at the placeholder backend
`zero-backend-not-deployed.tldraw.com`, which doesn't exist for the
preview.

This takes option 2 from the issue (deploy Zero for previews) rather
than gating the opt-in client-side.

## Changes

- **`.github/workflows/deploy-dotcom.yml`** — "Determine zero deploy
target": any PR preview now sets `DEPLOY_ZERO=flyio` (single-node).
Removed the `preview-flyio-zero-deploy-please` label check, the
commented-out auto-label block, and the diff-based `apps/dotcom/`
fallback that resolved to `false`. `production`/`main` stay on
`flyio-multinode`.
- **`internal/scripts/deploy-dotcom.ts`** — `getZeroUrl()` preview case
now always returns the deployed fly URL; removed the dead
`zero-backend-not-deployed` preview branch.

## Pruning / cleanup

No prune changes needed: each preview creates `pr-<n>-zero-cache`, which
already matches `prune-preview-deploys.ts`'s `ZERO_CACHE_APP_REGEX` and
is destroyed daily once the PR has been closed for >2 days.

## Follow-ups

- Delete the now-unused `preview-flyio-zero-deploy-please` GitHub label
(not referenced anywhere in code).
- Every dotcom preview now provisions a fly `zero-cache` app + runs Zero
migrations against its Supabase branch DB — small added per-preview
cost/latency.

## Test plan

1. Open a PR and apply `dotcom-preview-please`.
2. Sign into the preview with a `@tldraw.com` account.
3. Confirm the console logs `[Zero] Using proper Zero (@tldraw.com
email)` against the preview's `pr-<n>-zero-cache.fly.dev` backend and
that Zero-backed features work.
…tldraw#8979)

In order to extend the tldraw.com → tldraw.dev logo link to signed-in
users, this PR makes the sidebar workspace logo an external link to
tldraw.dev, and routes both logo links through the shared `ExternalLink`
wrapper so they emit click analytics consistently. It follows up tldraw#8938,
which repointed the logo only on the anonymous editor panel (where the
logo sits in the top-left).

**Signed-in logo (new link).** Signed-in users never see the anonymous
panel's logo — for them the logo lives in the sidebar via
`TlaSidebarWorkspaceLink`, which until now was a non-clickable `<div>`.
It now renders `ExternalLink` to `https://tldraw.dev`, opening in a new
tab with `target="_blank"`, `rel="noopener noreferrer"`, and an
`aria-label` (the logo is image-only). It reuses the same
`utm_campaign=top-left-logo` value as tldraw#8938 so both logos roll up
together on tldraw.dev's side.

**Anonymous logo (tracking + consistency).** tldraw#8938 used a raw `<a>` for
the anon top-left logo, which had no click tracking. It now uses
`ExternalLink` too, so both surfaces emit PostHog events —
`top-left-logo-clicked` (anon) and `sidebar-logo-clicked` (signed-in) —
distinct names so the two can be segmented, while the shared UTM
campaign still attributes them together. This also moves `target`/`rel`
into the wrapper rather than hand-maintained attributes.

Relates to tldraw#8871. Follows up tldraw#8938.

### Change type

- [x] `improvement`

### Test plan

1. **Signed in** (`yarn dev-app`, then sign in): click the tldraw logo
at the top of the left sidebar.
2. **Anonymous** (logged out, shared file, or published file): click the
tldraw logo in the top-left of the editor.
3. In both cases confirm a new tab opens at
`https://tldraw.dev?utm_source=dotcom&utm_medium=organic&utm_campaign=top-left-logo`,
and the original tab stays on tldraw.com.
4. Inspect each logo's `<a>` element: `target="_blank"`, `rel="noopener
noreferrer"`, `aria-label="tldraw.dev"`.

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

### Release notes

- Link the tldraw logo in the signed-in sidebar to tldraw.dev.

### Code changes

| Section | LOC change |
| --- | --- |
| Apps | +15 / -8 |

Co-authored-by: Jessica Claire Edwards <[email protected]>
### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`
In order to stop the dotcom `not-logged-in.spec.ts › can sign out` e2e
test from intermittently failing, this PR de-flakes the sidebar sign-out
fixture.

The fixture located the button with a loose `getByText('Sign out')` and
clicked it without waiting for the Radix user-settings dropdown to
finish opening. Playwright could therefore click mid-animation ("element
is not stable") or match a node that re-portals and detaches ("element
is detached from the DOM").

The fix targets the stable testid the menu item already renders
(`dialog.sign-out`, from the `TldrawUiMenuItem` `${sourceId}.${id}`
convention) and waits for the dropdown menu content to become visible in
`openUserSettingsMenu()` before any child-item click, so child clicks no
longer race the open animation. The change is confined to the e2e
fixture; no project-wide `reducedMotion` override is used, so
`cookie-consent.spec.ts` (which relies on animations running) is
unaffected.

### Change type

- [x] `other`

### Test plan

1. From `apps/dotcom/client`, run the sign-out test repeatedly to
confirm the flake is gone:
   `yarn e2e --grep "can sign out" --repeat-each=5`
2. Or from the repo root: `yarn e2e-dotcom`.

- [x] End to end tests

### Code changes

| Section | LOC change |
| ------- | ---------- |
| Tests   | +3 / -1    |
In order to stop callers from hanging forever when a debounced function
is cancelled mid-flight, this PR makes `debounce(...).cancel()` reject
the pending promise with `Error('Debounced function was cancelled')`.
Follow-up to a review note on tldraw#8604, where `cancel()` was updated to
release captured arguments but the pending promise was still left
unsettled.

Previously, `cancel()` cleared the pending timeout but never resolved or
rejected the in-flight promise — so any code that did `await
debouncedFn(...)` and then called `.cancel()` (or had it called
externally, e.g. via a hook cleanup) would hang forever. To avoid
breaking the common fire-and-forget pattern (`useZoomCss`,
`Editor.deepLink`, search debouncers, bookmark debouncer, VS Code change
responder, etc.), an internal no-op `.catch` is attached so callers that
discard the promise don't trigger unhandled-rejection warnings.
Consumers that `await` or chain `.then` / `.catch` on the returned
promise still observe the rejection through their own chain.

### Change type

- [x] `bugfix`

### Test plan

- [x] Unit tests

New tests cover:

- `cancel()` rejects all pending callers from the same debounce window
with the expected error.
- A fresh call after `cancel()` gets a brand-new promise (and verifies
state is reset cleanly).
- `cancel()` is a no-op when nothing is pending; double-cancel is safe.
- No `unhandledRejection` event fires when callers discard the promise
and call `cancel()` (verifies the fire-and-forget pattern is
unaffected).

### Release notes

- Fixed `debounce(...).cancel()` leaving any in-flight promise
unsettled. Calling `cancel()` while a debounced call is pending now
rejects the returned promise with `Error('Debounced function was
cancelled')`. Code that `await`s a debounced call across a cancel should
wrap it in `try/catch`.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +21 / -3   |
| Tests     | +62 / -0   |

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

---------

Co-authored-by: Copilot <[email protected]>
… note (tldraw#8958)

In order to make list editing in note shapes behave like text and geo
shape labels, this PR stops `Tab` from creating a new adjacent note when
the caret is inside a bullet or ordered list. Closes tldraw#8713.

Previously the note's keydown handler always created a new note on
`Tab`, while the rich text editor's own keymap *also* indented the list
item on the original note. This produced two bugs: an unwanted new note
was created and focused, and the original note's list item was silently
indented. Now, when a bullet or ordered list is active, the handler
bails out early and lets the rich text editor indent the list item.

### Change type

- [x] `bugfix`

### Test plan

1. Create a note shape and start editing it.
2. Open the rich text toolbar and turn on a bullet list (or ordered
list).
3. Type some text, press `Enter` to start a second item, then press
`Tab`.
4. Confirm the second item is indented under the first and no new note
is created.
5. With no list active, confirm `Tab` still creates an adjacent note as
before.

### Release notes

- Fixed a bug where pressing `Tab` while editing a list inside a note
shape created a new note instead of indenting the list item.
…raw#8980)

Fixing the app example page for custom colors: the example did not take
reloading into account and when a shape is created with a custom style
unfortunately it crashes the app because we don't persist the new
additions to palette and fonts

### Change type

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

### Test plan

1. go to example `/color-picker`
2. add a colour in the palette and some fonts
3. create a shape with new colour and font
4. reload 

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

### Release notes

- Fixed a bug in color-picker example that occurs when reloading the
canvas without persisting the added styles

Co-authored-by: Guillaume <[email protected]>
In order to actually enforce `tldraw/no-exported-arrow-const` everywhere
it was originally intended, this PR removes the three `.oxlintrc.json`
overrides that turned it off for `packages/editor`, `packages/tldraw`,
`packages/utils`, `apps/examples`, and `templates`, and converts the 50
violations those overrides were hiding.

The pattern is mechanical: `export const fn = (args) => {...}` becomes
`export function fn(args) {...}`. Async functions get `export async
function`. For React component aliases like `const X:
TLErrorFallbackComponent = (...) =>` the form becomes `export const X:
TLErrorFallbackComponent = function X(...) {...}` — a named function
expression keeps the type alias visible to api-extractor (the rule only
fires on arrows, not function expressions). No behavior changes.

### Affected directories

| Directory | Files | Violations |
| --- | ---: | ---: |
| `packages/tldraw` | 14 | 19 |
| `apps/examples` | 8 | 16 |
| `packages/editor` | 10 | 11 |
| `packages/utils` | 4 | 4 |
| **Total** | **36** | **50** |

### Background

Relates to tldraw#7572. The rule was added in tldraw#8258 along with the JS plugin,
but with these overrides in place it has never fired in the packages it
most needed to apply to. The plugin itself works correctly — see commit
history for the exploration that confirmed that.

### API changes

The shift to function declarations changes how api-extractor renders the
affected exports — `const X: (args) => Y` becomes `function X(args): Y`.
No call-site changes required; the public signatures are equivalent.

Specifically affected `@public` exports:

- `@tldraw/editor`: `DefaultSvgDefs`, `isSafeFloat`,
`stopEventPropagation`
- `@tldraw/tldraw`: `handleNativeOrMenuCopy`,
`TldrawUiContextualToolbar`, `TldrawUiToolbarToggleGroup`,
`TldrawUiToolbarToggleItem`, `truncateStringWithEllipsis`,
`useSelectedShapesAnnouncer`, `interpolateSegments`
- `@tldraw/utils`: `Image`, `noop`, `safeParseUrl`, `isAvifAnimated`

`DefaultErrorFallback` keeps its `TLErrorFallbackComponent` alias in the
api-report — typed as `export const DefaultErrorFallback:
TLErrorFallbackComponent = function DefaultErrorFallback(...) {...}` so
the alias survives.

### Change type

- [x] `improvement`

### Test plan

- `yarn oxlint .` — 0 errors.
- `yarn typecheck` — passes.
- `yarn test run` in `packages/editor`, `packages/tldraw`,
`packages/utils` — all green.
- `yarn api-check` — passes (api reports are regenerated).

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

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Core code       | +97 / -88  |
| Automated files | +12 / -12  |
| Documentation   | +24 / -18  |
| Config/tooling  | +0 / -3    |
In order to keep `import { x } from 'lodash'` from silently pulling in
the full ~72 KB lodash library, this PR adds an oxlint
`no-restricted-imports` rule for the bare `lodash` package and migrates
the existing usages to `lodash/X` deep imports.

Lodash's main package is CommonJS — its `lodash.js` is a UMD wrapper
that `module.exports = _`, with no `module` or `exports` field. Neither
Rollup (Vite) nor esbuild can tree-shake that. Bundling a one-line entry
confirmed this on the version actually installed (`[email protected]`):

| Import shape                             | Vite (Rollup) | esbuild |
| ---------------------------------------- | ------------: | ------: |
| `import { throttle } from 'lodash'`      |       72.1 KB | 73.8 KB |
| `import _ from 'lodash'`                 |       72.6 KB | 73.8 KB |
| `import throttle from 'lodash/throttle'` |        4.2 KB |  3.7 KB |
| `import isEqual from 'lodash/isEqual'`   |       15.5 KB | 16.0 KB |

Deep imports (`lodash/X`) work because each method lives in its own file
in the main package and `require`s only the helpers it actually needs.
They tree-shake naturally under both bundlers, and we only need one
`lodash` dep + one `@types/lodash` per workspace instead of one per
method.

Affected:

- `apps/vscode/extension/src/WebViewMessageHandler.ts` was importing `{
isEqual }` from `lodash`, shipping the full ~72 KB library in the
extension bundle. Now uses `lodash/isEqual` (~16 KB).
- `apps/examples/*` had four files using bare `lodash`. Migrated
`throttle`, `isEqual`, `reduce`, and `capitalize` to `lodash/X` deep
imports. `lodash.round(x)` → `Math.round(x)` in `GlobShapeUtil.tsx`
because every call site is single-argument and behaves identically
without the precision parameter.
- `apps/dotcom/client/scripts/build.ts` was using `_.chunk` — replaced
with a 3-line inline `for` loop. The build script is Node-only (no
bundle impact) and chunking has no edge cases here, so it didn't seem
worth adding `lodash` + `@types/lodash` to `apps/dotcom/client` for one
call site.
- `packages/state`'s fuzz test was using `lodash.times` — now
`lodash/times`.
- Dropped truly-unused `lodash` / `lodash.throttle` deps from
`packages/state-react` and `templates/sync-cloudflare`.

The new lint rule restricts only the bare `'lodash'` specifier, so
`lodash/X` deep imports, `lodash-es`, and the existing `lodash.<method>`
per-method packages (`packages/utils`, `apps/dotcom/sync-worker`,
`apps/dotcom/client`'s `lodash.pick`) all still pass.

### Change type

- [x] `improvement`

### Test plan

1. `npx oxlint` from the repo root → 0 errors across the codebase.
2. With a planted `import { capitalize } from 'lodash'`, the rule fires
with a message pointing to `lodash/X` deep imports, `lodash-es`, and
per-method packages.
3. `yarn typecheck` → clean.
4. `packages/state` fuzz tests → 21/21 pass.

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

### Release notes

- Shrink the VS Code extension bundle by ~56 KB by switching `isEqual`
off of the full lodash package.

### Code changes

| Section        | LOC change |
| -------------- | ---------- |
| Core code      | +0 / -2    |
| Tests          | +1 / -1    |
| Documentation  | +14 / -14  |
| Apps           | +6 / -3    |
| Templates      | +0 / -2    |
| Config/tooling | +17 / -4   |
…aw#8976)

## Overview

In order to keep an in-progress interaction intact when content is
pasted mid-gesture, this PR stops `defaultHandleExternalTldrawContent`
from selecting the pasted shapes while the user is dragging a handle,
translating, resizing, or rotating.

This is an **alternative** to tldraw#8968 for the same underlying problem
(tldraw#8305). Where tldraw#8968 keeps the binding-hint overlay alive by gating the
overlay's `isActive`/`getOverlays` on the interaction state, this PR
instead avoids the root cause for the paste case: the pasted content no
longer steals selection from the shape being manipulated. Opened as a
draft so the two approaches can be compared side by side.

When the user pastes tldraw content while mid-interaction — for example
dragging an arrow's endpoint handle — the default handler used to
`select` the newly pasted shapes. That stole selection from the shape
under manipulation and interrupted the gesture (the arrow's in-progress
binding hint, for instance, disappeared until the drag ended). With this
change the selection is left untouched while mid-interaction, so the
gesture continues uninterrupted. When not mid-interaction, paste still
selects the pasted content as before.

Because we no longer reselect mid-interaction, the selection bounds
before and after paste are identical, which would make the paste "puff"
overlap check always pass. The puff is meant to signal that the
newly-selected pasted content landed on the old selection, so it's now
skipped while mid-interaction.

## Scope note

The known undo-grouping quirk tracked in tldraw#8969 (paste landing in the
same undo entry as the drag) is **intentionally left out of scope**
here, to keep this change focused and comparable to tldraw#8968.

Thanks to @SomeHats for suggesting this approach in
tldraw#8968 (comment).

## Change type

- [x] `bugfix`

## Test plan

1. Create a shape and an arrow bound to it.
2. Start dragging the arrow's free endpoint handle.
3. While still dragging, paste some tldraw content.
4. The arrow stays selected and its binding hint remains visible; the
pasted content is added without stealing selection.
5. Paste while idle — the pasted content is selected as normal.

- [x] Unit tests added in `SelectTool.test.ts` covering dragging a
handle, translating, resizing, the no-puff case, and the idle
(still-selects) case.

## Release notes

- Pasting content while dragging, translating, resizing, or rotating no
longer steals selection from the shape being manipulated.

## Code changes

| Area | Files | +/- |
| --- | --- | --- |
| Core | `defaultExternalContentHandlers.ts` | +19/-1 |
| Tests | `SelectTool.test.ts` | +134/-0 |

Relates to tldraw#8305. Alternative to tldraw#8968.
Partially inspired by customer request, partially to showcase new
framelike behavior, this example adds a drag-and-droppable container
that uses native css flexbox as the layout engine.


https://github.com/user-attachments/assets/508d8f8e-bb33-45f1-b9e9-87c1d18d5b63


### Change type

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

### Test plan

1. Open the flex layout example.
2. Select the frame background and use the contextual toolbar to toggle
horizontal/vertical layout.
3. Drag the outside green rectangle into the layout and verify the
hint/drop line, pointer-up reparenting, and growth.
4. Drag a child out and verify it deparents and the layout shrinks
during mouse move before pointer-up.

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

### Release notes

- Added a flex layout custom shape example.

<sub>To show artifacts inline, <a
href="https://cursor.com/dashboard/cloud-agents#team-pull-requests">enable</a>
in settings.</sub>
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-d6af83b5-a09f-4f7a-abc7-0a597d9d6e13"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-d6af83b5-a09f-4f7a-abc7-0a597d9d6e13"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor <[email protected]>
In order to keep the next release notes aligned with what will ship in
v5.1.0, this updates `apps/docs/content/releases/next.mdx` from the
`production` branch (freeze week). All 14 existing entries are present
on production, so no stale entries needed pruning and no archival was
required (`last_version` stays at v5.0.2).

Two new editor performance entries were added to the Improvements
section — skipping spatial index updates on prop-only shape diffs
(tldraw#8799) and tiering `notVisibleShapes` per-shape subscriptions (tldraw#8804).
These are complementary changes that improve drawing performance on
pages with many shapes, so they're grouped into a single entry. The
intro paragraph now mentions canvas performance improvements.

Other production PRs since the last update were skipped per the style
guide: same-cycle page-menu polish (tldraw#8927, tldraw#8924, which fix the
in-release page-menu redesign), internal refactors and manager hardening
(tldraw#8833, tldraw#8895), examples-only additions (tldraw#8914, tldraw#8916, tldraw#8837), and the
usual infra/chore/dotcom/docs/CI changes.

### Change type

- [x] `other`

### Test plan

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

### Code changes

| Section       | LOC change |
| ------------- | ---------- |
| Documentation | +2 / -1    |
…ldraw#9011)

In order to ship the upcoming release without the in-progress "press q
to copy hovered styles" feature, this PR reverts tldraw#8917.

The original feature added a `copy-hovered-styles` action that let users
press `q` while hovering a shape to copy its styles onto the
next-created shape. We want to release without it, so this revert
removes the action, its keyboard shortcut entry, and the associated
translation key.

The only non-trivial part of the revert was a conflict in the generated
`api-report.api.md`: a later PR added the `page-menu.max-pages-reached`
translation key in the same union type. That key is preserved; only
`action.copy-hovered-styles` was removed.

### Change type

- [x] `other` (revert)

### Test plan

1. Hover a shape, press `q`, then draw a new shape.
2. Confirm the hovered shape's styles are no longer copied (the keyboard
shortcut is gone).

### Release notes

- Remove the "press q to copy hovered styles" action.

### API changes

- Removed `'action.copy-hovered-styles'` from the `TLUiTranslationKey`
union.

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Core code       | +0 / -33   |
| Automated files | +1 / -1    |
| Config/tooling  | +0 / -1    |
…ckout (tldraw#9013)

In order to make our failure alerts reliable, this PR ensures the
Discord notify step can always find its local action. The notify step
uses a local action (`./.github/actions/discord-fail-notify`), but
several publish workflows run steps before `Check out code` (e.g.
`Determine publish mode`, `Generate GH token`). When one of those fails,
checkout is skipped, so the local action can't be found and the failure
notification itself fails — as seen in [this
run](https://github.com/tldraw/tldraw/actions/runs/26878352925/job/79271600922):

```
Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '.github/actions/discord-fail-notify'.
Did you forget to run actions/checkout before running your local action?
```

The fix adds a `failure()`-gated sparse checkout of just the action
immediately before each notify step, so the action is always present
regardless of where the failure occurred. It's a no-op on success.

A scan of all workflows found four with this pattern (a `failure()`
local-action notify after a step that runs before checkout):
`publish.yml`, `bump-versions.yml`, `publish-editor-extensions.yml`,
`publish-vscode-extension.yml`. Other workflows using the action run
checkout as their first step, so they're unaffected.

### Change type

- [x] `other`

### Test plan

- Cannot be meaningfully tested without deliberately failing a publish
workflow; verified by inspecting step ordering across all workflows.

### Code changes

| Section        | LOC change |
| -------------- | ---------- |
| Config/tooling | +28 / -0   |
In order to stop every new auth surface from having to render its own
CAPTCHA target, this PR hoists Clerk's `#clerk-captcha` element to the
always-mounted app root (`TlaRootProviders`) so any Clerk client call
anywhere in the app can attach its challenge. Closes tldraw#8951.

Previously the element only existed inside `TlaSignInDialog` (added in
tldraw#7105), so any Clerk call outside that dialog — a resend after the
dialog closes, or future flows like adding an email, MFA, passkeys
(tldraw#8774), or password reset — could fail with a CAPTCHA error in
production. The element now lives inside the themed `.tla` container for
the lifetime of the page, the dialog-scoped duplicate is removed (Clerk
only uses the first matching element), and global styling keeps an
invisible challenge hidden while centering a visible one over the app.

### Change type

- [x] `improvement`

### Test plan

1. Sign in / sign up with email and confirm the verification flow still
works, including the invisible CAPTCHA path.
2. Trigger a resend after closing the sign-in dialog and confirm the
Clerk call no longer errors for a missing CAPTCHA target.
3. Inspect the DOM and confirm a single `#clerk-captcha` is present at
the app root and that the empty element is invisible / takes no space.

### Release notes

- Mount the Clerk CAPTCHA target globally so authentication flows
outside the sign-in dialog no longer fail with a CAPTCHA error.

### Code changes

| Section | LOC change |
| ------- | ---------- |
| Apps    | +21 / -8   |
In order to stop a two-finger pinch from leaving the selection changed
on touch devices, this PR captures the pre-gesture selection on the
first `pointer_down` and rolls back any incidental change when the pinch
starts. Fixes tldraw#8397.

On touch, the first finger of a pinch fires a `pointer_down` that can
change the selection (for example by landing on a different shape)
before the second finger starts the gesture. The snapshot used to roll
this back was taken at `pinch_start`, by which point the change had
already happened, so the incidental selection persisted after the pinch.
Now the snapshot is taken on the first `pointer_down` only, and
`pinch_start` keeps that snapshot rather than overwriting it. When no
`pointer_down` precedes the pinch — Safari delivers trackpad pinches as
gesture events — `pinch_start` still captures the live selection, so the
Safari fix from tldraw#6907 / tldraw#7777 is preserved.

The rollback happens at `pinch_start` (as soon as we know it's a pinch),
not at `pinch_end`, so the pre-gesture selection is what's shown
*during* the pinch rather than the wrong shape appearing selected for
the whole gesture and snapping back on release.

The regression dates to tldraw#7777 (which made the `pinch_start` snapshot
unconditional to fix a Safari case) and was surfaced by the
gesture-handling rewrite in tldraw#8392. A deeper follow-up — unifying pinch
detection and selection onto a single pointer event stream — is tracked
separately.

### Change type

- [x] `bugfix`

### Test plan

Three layers of coverage, each of which fails on the pre-fix behavior
and passes with the fix:

- `packages/tldraw/src/test/pinch.test.ts` — unit tests via
`TestEditor`: rollback at pinch start (not just end), empty selection,
consecutive pinches, the Safari/no-pointer-down path, and the editing
guard.
- `packages/tldraw/src/test/pinch-dom.test.tsx` — integration tests that
drive the real canvas handlers (`useCanvasEvents`, `useGestureEvents`)
with native `pointerdown` and `touchstart`/`touchmove`/`touchend`
events, including the case where the second finger's `pointer_down` is
processed before `pinch_start`.
- `apps/examples/e2e/tests/test-camera.spec.ts` — a Mobile Chrome e2e
that dispatches a real two-finger touch pinch via CDP
`Input.dispatchTouchEvent`, asserting the selection is rolled back both
during and after the pinch.

Manual check on a touch device: select a shape, start a two-finger pinch
with the first finger landing on a different shape, zoom, lift both
fingers — the originally selected shape stays selected throughout.

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

### Release notes

- Fix a two-finger pinch unintentionally changing the selection on touch
devices.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +39 / -6   |
| Tests     | +376 / -0  |
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 tldraw#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 <[email protected]>
…flame chart (tldraw#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
tldraw#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 |
…w#9003)

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 tldraw#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 tldraw#8888 ("`editor.blur({ blurContainer: false })`
bypasses the manager") was already resolved separately by tldraw#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 tldraw#8888
huppy-bot Bot and others added 24 commits July 1, 2026 10:33
…ldraw#9390)

Tier 3 of the socket.dev dependency cleanup (follows tldraw#9366, tldraw#9381,
tldraw#9389). The judgement-call batch: deps that the in-range pass (tldraw#9389)
couldn't reach because consumers pin them to exact or tilde ranges, plus
the grpc and dev-tooling clusters.

## Change type

- [x] `other`

## `resolutions` overrides (force same-major fixes)

These were stuck at vulnerable versions by pins like `ws@~8.17.1`,
`[email protected]`, `[email protected]`. All forced versions are within the
same major, so API-compatible.

| Package | From → To | Advisory |
|---|---|---|
| ws | 8.17.1 → 8.21.0 | CVE-2026-48779, CVE-2026-45736 |
| js-cookie | 3.0.5 → 3.0.7 | CVE-2026-46625 |
| postcss | 8.4.31 → 8.5.10 | CVE-2026-41305 |
| react-router / @remix-run/router | 6.30.1 → 6.30.4 / 1.23.0 → 1.23.2 |
CVE-2026-22029, CVE-2026-40181, CVE-2025-68470 |
| @octokit/plugin-paginate-rest | 11.3.1 → 11.4.1 | CVE-2025-25288 |

## In-range bumps (`yarn up -R`, no override)

| Package | From → To | Advisory |
|---|---|---|
| @grpc/grpc-js | 1.12.6 → 1.14.4 | CVE-2026-48068, CVE-2026-48069 |
| form-data | 4.0.5 → 4.0.6 | CVE-2026-12143 |
| @babel/core | 7.29.0 → 7.29.7 | CVE-2026-49356 |
| @isaacs/brace-expansion | 5.0.0 → 5.0.1 | CVE-2026-25547 |

## Knowingly left open

- **Dev-only deps pinned to old majors** — `fast-xml-parser@4` (incl.
the 9.3 critical, dev scope), `lodash@4`, `serialize-javascript`, `tmp`,
`xml2js`, `nanoid@3`. Fixing these needs forced **major** overrides that
risk breaking the build/test toolchain for no shipped-code benefit.
Better suppressed via Socket dashboard ignore.
- **`[email protected]`** — still deferred; consumers (`cacache`, `node-gyp`,
`sqlite3`) need the tar v6 API.
- **OpenTelemetry 0.203 cluster** — fix is a 0.x minor (effectively
breaking); wants a deliberate, tested bump.
- **`[email protected]`** — Socket's suggested "fix" is a downgrade to 3.2.3; no
valid forward fix.

## Test plan

- `yarn typecheck` passes.
- Pre-commit hooks pass.
- ⚠️ These are forced/transitive bumps not exercised by typecheck alone
— worth a CI run (and a smoke of the docs site for
`react-router`/`postcss`, and the workers for `@grpc/grpc-js`) before
merge.
…aw#9458)

Closes tldraw#9457

This happens because we're initialising the dragging with a previous
drag set to the same initial drag, which means there no change in
location and so we end up not parenting the shape.

### Change type

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

### Test plan

1. Create a frame and move it under the toolbox
2. Drag a new shape from the toolbox to the frame
3. The frame should indicates the new shape will be parented to it

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

### Release notes

- Fixed the dragging of a shape from the toolbox to a frame when the
frame is under the toolbox, the shape is now properly parented to the
frame
…#9455)

Closes tldraw#8078

We're missing the firing of `onChildrenChange` when creating a shape
passing the parent id.
After a shape is created, it was not gathered for side effects at all
and this is why it was not firing the callback.

This fix was already proposed in
tldraw#8172 but @max-drake figured it was
now firing twice… but in reality it was because we were firing once for
creating the shape and a second time by resizing the shape, this is a
different issue and in some ways I believe this is correct behaviour.
Should we batch the changes in order to fire once is a follow-up
question to this PR

### Change type

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

### Test plan

Reproduction case in issue

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

### Release notes

- Fixed missing `onChildrenChange` call when directly creating a shape
within a parent shape.
In order to bring the tldraw.com error screens in line with the new
design, this PR restyles every dotcom error surface to a minimal
centered block: a semibold title, medium body text, and an underlined
text-link action, with the large sad-face icon removed. Placement of
each error is unchanged — some render inside the app shell, others
full-page.

- `ErrorPage` and `TlaFileError`: remove the icon, add a `tla-error`
container test marker, and convert primary-button CTAs to underlined
text links.
- `StoreErrorScreen` and `TlaRootProviders`: convert the refresh CTAs to
text links.
- `globals.css` and `TlaFileError.module.css`: new compact type scale
(12px semibold title, 12px medium body, 19.2px line-height), 12px gap,
underlined link styling.
- e2e: re-anchor the "error is showing" assertions from the removed
`tla-error-icon` to the new `tla-error` container marker.

`sadFaceIcon` is still exported since `TlaInviteExpiredDialog` (not an
error screen) continues to use it.

### Change type

- [x] `improvement`

### Test plan

1. Trigger a 404 (visit a non-existent page) and confirm the redesigned
error block.
2. Open a file you don't have access to and confirm the "invite only" /
"sign in" states render with underlined link CTAs.
3. Force a client-too-old / update state and confirm the "refresh" link
works.

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

### Release notes

- Redesign the tldraw.com error screens with a cleaner, more compact
layout.

### Code changes

| Section | LOC change |
| ------- | ---------- |
| Tests   | +8 / -8    |
| Apps    | +57 / -41  |
…#9462)

In order to simplify the sign-in flow, this PR removes the analytics
opt-in toggle from the terms of use acceptance dialog and restyles that
dialog. It adds an accessible "Accept terms of use" dialog title (kept
in the DOM for the dialog's accessible label but visually hidden), drops
the tldraw logo, and uses a dedicated compact layout. Analytics consent
is unaffected elsewhere (the cookie consent banner still handles it).

- `TlaLegalAcceptance`: remove the analytics consent toggle and its
persistence on accept; switch to the new `legal*` dialog styles and add
an accessible (visually hidden) dialog title.
- `auth.module.css`: add the legal dialog layout styles
(`legalContainer`, `legalBody`, `legalDescription`, `legalCtaButton`);
`legalDialogHeader` collapses the header so the title labels the dialog
without being shown.
- e2e: drop the now-unused analytics-toggle fixtures from
`SignInDialog`.

### Change type

- [x] `improvement`

### Test plan

1. Sign in as a user who has not accepted the legal terms.
2. Confirm the acceptance dialog shows the centered terms of use /
privacy policy description and the "Accept and continue" button, with no
analytics toggle and no visible title heading.
3. Accept and continue; confirm acceptance is stored and the dialog
closes.

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

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Apps            | +46 / -44  |
| Tests           | +0 / -24   |
| Automated files | +9 / -23   |
In order to stop exported PNGs and SVGs from cutting off italic,
right-to-left, and cursive glyph ink at a text shape's edge (tldraw#8802),
this PR pads the exported `<foreignObject>` in `TextShapeUtil.toSvg` by
a fraction of the font size and insets the text by the same amount.
Glyph ink that spills past the advance box — italic side bearings,
slanted ascenders, cursive tails — now renders into that margin instead
of being clipped at the foreignObject boundary, and the export's
existing content-trim (`getSvgAsImage`) crops the slack back to the real
pixels, so the image stays tight. The change is export-only: the shape's
geometry — selection bounds, hit-testing, and the text-edit cursor —
still uses the advance box, so nothing on-canvas moves.

Before

<img width="587" height="595" alt="image"
src="https://github.com/user-attachments/assets/fe15923f-c890-4ce2-aec1-01c94e5c4e85"
/>

After

<img width="558" height="567" alt="image"
src="https://github.com/user-attachments/assets/1ac8fde0-4089-4ecc-8769-5169cd062ed3"
/>

### Change type

- [x] `bugfix`

### Test plan

1. Create an italic (or Arabic / cursive) text shape whose glyphs lean
past the box.
2. Export it as a PNG, then as an SVG, from the export menu.
3. The exported image encloses the full glyphs, with no clipped edges.

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

### Release notes

- Fix exported PNG and SVG images clipping the edges of italic,
right-to-left, and cursive text.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +14 / -2   |
| Tests     | +107 / -0  |

Closes tldraw#8802

---------

Co-authored-by: huppy-bot[bot] <128400622+huppy-bot[bot]@users.noreply.github.com>
In order to make arrows bound to frames behave like frame contents, this
PR parents arrows to a frame-like shape when both arrow terminals are
bound to that same frame-like shape, or when one terminal is bound to
the frame-like shape and the other is bound to a descendant inside it.
It also keeps arrows from being clipped by frame-like ancestors. Closes
tldraw#8978.

<img width="1168" height="1161" alt="image"
src="https://github.com/user-attachments/assets/62550107-cb83-4f67-9f8b-9235fb9752ea"
/>

### Deletion behavior

When the inner frame is deleted, these shapes are deleted:

- The inner frame
- The shape inside the inner frame
- Arrow D, because it is parented to the inner frame

These shapes are not deleted:

- Arrows A, B, C, E, F, and G
- Arrow F remains because `[outer frame, inner frame]` parents to the
outer frame
- Arrow G remains because it is page-parented; it loses its binding to
the deleted inner shape

When the outer frame is deleted, these shapes are deleted:

- The outer frame
- The inner frame
- The shape inside the inner frame
- Arrows B, C, and E, because they are parented to the outer frame
- Arrow F, because `[outer frame, inner frame]` parents to the outer
frame
- Arrow D, because it is parented to the inner frame, which is a
descendant of the outer frame

These shapes are not deleted:

- Arrow A, because it is page-parented with only one bound end
- Arrow G, because it is page-parented; it loses its binding to the
deleted inner shape

### Change type

- [x] `bugfix`

### Test plan

1. Draw a frame and bind both ends of an arrow to it.
2. Duplicate and delete the frame, and verify the arrow duplicates and
deletes with it while remaining visible outside the frame bounds if
needed.
3. Draw arrows for the parent cases `[frame a, frame a]`, `[frame a,
descendant of frame a]`, and `[frame a, frame b]`, and verify they
parent to frame a, frame a, and the page respectively.

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

### Release notes

- Fix arrows bound to frames so they duplicate and delete with the frame
without being clipped.

### API changes

- Added `BaseFrameLikeShapeUtil.shouldClipChild()` default behavior that
excludes arrow children from frame-like clipping.

### Code changes

| Section | LOC change |
| --------------- | ---------- |
| Core code | +30 / -3 |
| Tests | +148 / -3 |
| Automated files | +2 / -0 |
Closes tldraw#7692

updateNode `isOutOfDate` param defaults to true (shared.tsx:92), but
SliderNode passes false, so the slider marks itself not dirty and
downstream nodes never recompute.

### Change type

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

### Test plan

1. Run the workflow template
2. Create a slider
3. Create an "add" block
4. Connect slider to "add"
5. Run the workflow,
6. Update the slider

The slider out node is grey-ed as well as its connection on the "add"
block

### Release notes

- Fixed the workflow template so dirty nodes are now shown correctly
when updating sliders
In order to let code that only holds the `editor` object check whether
the editor has mounted, this PR adds an `Editor.isMounted` flag —
mirroring the existing `Editor.isDisposed`.

The editor already exposes both a `dispose` event and an `isDisposed`
flag, but for mount there was only the `mount` event and no flag.
Because `mount` is fire-once, code that obtained the editor after it had
already mounted (the common case) had no way to check whether mount had
happened. `isMounted` closes that gap:

```ts
if (editor.isMounted) runNow()
else editor.once('mount', runNow)
```

This is aimed at making it easier to script against a live editor from
outside (e.g. posting scripts to an exec endpoint), where you need a
synchronous "has it mounted yet?" check rather than racing the fire-once
event. Reacting to the transitions is still done via the `mount` /
`dispose` events, matching how `isDisposed` works.

### API changes

- Added `Editor.isMounted`, set to `true` once the editor's `mount`
event has fired. Mirrors `Editor.isDisposed`.

### Change type

- [x] `api`

### Test plan

1. Mount a `<Tldraw>` / `TldrawEditor` and confirm `editor.isMounted` is
`true` in `onMount` and after.

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

### Release notes

- Add `Editor.isMounted` to check whether the editor has mounted,
mirroring `Editor.isDisposed`.

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Core code       | +7 / -0    |
| Automated files | +1 / -0    |
In order to preserve the clipped-arrow selection fix from tldraw#9429 after
tldraw#9470 moved frame-bound arrows out of clipping, this PR lets
already-selected shapes be hit-tested against their geometry using the
standard hit-test margin when deciding whether a drag should start. This
keeps selected arrows grabbable at the same places as their selection
indicator, including for custom clipping containers that still clip
arrow children.

### Why this change is needed

Normal shape picking still goes through `getShapeAtPoint`, which uses
the editor's standard hit-test margin and respects clipping masks. That
is the right behavior for finding the top-most visible shape under the
pointer.

The selected-shape path is different. `getSelectedShapeAtPoint` runs
after a shape is already selected, and it decides whether a pointer down
should start dragging that selected shape. Before this PR, it used
`isPointInShape` with `margin: 0`. That worked for many selected clipped
shapes because closed shapes have an inside area and this path passed
`hitInside: true`.

Arrows are different: they are open, thin geometry, so `margin: 0`
required an exact hit on the arrow body. For a curved arrow that is
already selected and visually represented by its selection indicator,
pointer down near the selected arrow could miss the selected shape and
start another canvas interaction instead.

This PR keeps `isPointInShape` as the clipped/visible-shape test used by
normal picking, and makes `getSelectedShapeAtPoint` use the selected
shape's own geometry with the standard hit-test margin. The result
matches the interaction model: normal picking still uses clipped shape
hit-testing, while already-selected arrows remain draggable with the
same forgiving margin as regular arrow hits.

### Change type

- [x] `bugfix`

### Test plan

1. Use a custom clipping parent with a curved arrow child that extends
outside the clip path.
2. Confirm a normal shape hit-test misses the clipped point, but the
selected arrow can be dragged from that point.

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

### Release notes

- Fix selected clipped arrows so they can be dragged where their
geometry extends outside a clipping parent.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +19 / -4   |
| Tests     | +49 / -0   |

---------

Co-authored-by: Guillaume <[email protected]>
…i, ip-address) (tldraw#9476)

In order to clear the CVEs that socket.dev disclosed after the tier 1–3
dependency cleanup (tldraw#9366, tldraw#9381, tldraw#9389, tldraw#9390) landed, this PR bumps
three production transitive deps to versions already reachable in the
tree. Follows the same risk logic as tldraw#9390: only in-range bumps and
same-major `resolutions` overrides, no forced majors.

### Cleared CVEs

**In-range bump (`yarn up -R`)**

| Package | From → To | Advisory |
|---|---|---|
| dompurify | 3.4.10 → 3.4.11 | permanent `ALLOWED_ATTR` pollution via
`setConfig()` |

**`resolutions` overrides (retire stale exact pins; same-major,
API-compatible)**

| Package | From → To | Advisory |
|---|---|---|
| undici | 7.24.4 → 7.28.0 | CVE-2026-9697, CVE-2026-12151,
CVE-2026-6734, CVE-2026-9678/9679, CVE-2026-11525, CVE-2026-6733 |
| ip-address | 10.1.0 → 10.2.0 | CVE-2026-42338 |

Both overrides just collapse a stale exact pin onto a version the rest
of the tree already resolves (`undici@^7` → 7.28.0, `ip-address@^10.1.1`
→ 10.2.0), so there is no net API change.

### Knowingly left open (unchanged from tldraw#9390's reasoning)

- **[email protected]** — consumers (`cacache`, `node-gyp`, `sqlite3`) need the
v6 API.
- **[email protected]** — Socket's suggested "fix" is a downgrade to 3.2.3; no
valid forward fix.
- **@opentelemetry/core 2.7.1** — pinned by the `0.218.0` suite; forcing
only `core` to 2.8.0 risks a runtime version mismatch. Wants a
deliberate, tested suite bump.
- **uuid@8** (fix is major v11), **qs@`<6.10`** (fix is 6.14, out of the
consumer's range) — need forced majors / out-of-range overrides.
- **Dev-only pins** (fast-xml-parser, lodash, serialize-javascript,
minimatch, ajv, tmp, xml2js, nanoid) and Socket's **obfuscated-code**
warnings — better suppressed via the Socket dashboard than via forced
major overrides on the toolchain.

### Change type

- [x] `other`

### Test plan

- `yarn typecheck` passes.
- ⚠️ Forced/transitive bumps aren't fully exercised by typecheck alone —
worth a CI run (and a smoke of anything using `dompurify` sanitization)
before merge.

### Code changes

| Section        | LOC change |
| -------------- | ---------- |
| Config/tooling | +11 / -23  |
Adds a `repository_dispatch` from `publish.yml` to
**`tldraw/tldraw-internal`** so the internal monorepo keeps its pinned
tldraw SDK version up to date automatically — the event-driven
replacement for an hourly poll it runs today.

## API changes

- [x] `other`

## What this does

Right after publishing, `publish.yml` already notifies `tldraw-desktop`.
This mirrors that for `tldraw-internal`, adding two steps gated on
**`mode == 'next'`** (the prerelease channel the internal monorepo
tracks):

- `Generate tldraw-internal token` — a huppy app token scoped to
`tldraw-internal`.
- `Notify tldraw-internal` — `POST
/repos/tldraw/tldraw-internal/dispatches` with `event_type:
tldraw-release`.

## ⚠️ Prerequisite: app access

Reuses the existing `HUPPY_APP_ID` / `HUPPY_PRIVATE_KEY`, but the huppy
GitHub App must be **granted access to the `tldraw-internal` repo** (it
currently only lists `tldraw-desktop`). Without that grant, the token
step fails.

## Companion PR

The receiving workflow: **tldraw/tldraw-internal#637** (listens for
`repository_dispatch: types: [tldraw-release]`). The `event_type` string
matches exactly on both ends.

> Side note: the existing `Notify tldraw-desktop` step appears to be
dispatching into the void — it sends `event_type: tldraw-release`, but
`tldraw-desktop`'s only workflow now listens for `types: [release]`.
Worth a separate fix if desktop auto-update is still expected to work.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
…mode (tldraw#9225)

In order to stop the app from crashing when a user drags a tool out of
the toolbar in a read-only room, this PR makes
`onDragFromToolbarToCreateShape` tolerate the case where the shape isn't
created. Closes tldraw#9116.

In read-only mode `editor.createShape` is a no-op, so the helper created
a shape id, asked for it back, and then
`assertExists(editor.getShape(id), 'Shape not found')` threw an
unhandled error and crashed the app (`willCrashApp: true`). This
surfaced on tldraw.com, where `useExtraToolDragIcons` attaches
`onDragStart` to the select/hand/draw tools — and those tools stay
visible in read-only mode because they're `readonlyOk`.

The fix:

- Bail out early when the editor is read-only, before any history mark
or tool change, so a read-only drag does nothing.
- Replace the `assertExists` invariant with a graceful rollback: if
shape creation doesn't take effect for any other reason, bail to the
history mark, return to `select.idle`, and exit instead of throwing.

### Change type

- [x] `bugfix`

### Test plan

1. Open a read-only editor that wires `onDragStart` onto a `readonlyOk`
tool (e.g. the select tool), as tldraw.com does.
2. Drag that tool out of the toolbar onto the canvas.
3. The app no longer crashes; nothing is created and the editor stays in
`select.idle`.

- [x] Unit tests

### Release notes

- Fix a crash ("Shape not found") when dragging a tool from the toolbar
in a read-only room.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +11 / -2   |
| Tests     | +84 / -0   |

---------

Co-authored-by: Guillaume <[email protected]>
Co-authored-by: Guillaume Richard <[email protected]>
…raw#9488)

In order to fix tldraw#8898, where double-clicking a shape inside an unfocused
group immediately started editing it, this PR makes a double click
behave like two clicks: at any focus layer it drills one level deeper
into the group hierarchy instead of jumping straight to editing. The
first double click on a shape inside an unfocused group selects the
group, the next selects the shape inside it, and — if that shape is
itself a group — the next drills again. Only once a shape is reachable
at the focused layer does a double click edit (or crop) it. Groups
always drill; frames and the page aren't focus layers, so their children
still edit directly. The pattern resets whenever the focus layer
changes.

The `shape` double-click case in `Idle` now owns this decision in one
place, and the `canvas` path just re-dispatches to it. This replaces the
previous nested group/parent special-casing, which also lacked a guard
on the `shape` path (the cause of tldraw#8898) and skipped intermediate levels
in nested groups.

### Change type

- [x] `bugfix`

### Test plan

1. Create two shapes, group them, and deselect everything.
2. Double-click a shape inside the group. The shape inside the group is
selected and the group becomes the focused layer, but the shape does not
start editing.
3. Double-click the same shape again. Now it enters editing mode.
4. Nest that group inside another group. Each double click drills one
level deeper before editing.
5. Put a shape inside a frame and double-click it. It edits directly,
since frames are not focus layers.

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

### Release notes

- Fix double-clicking a shape inside an unfocused group immediately
starting to edit it. A double click now drills one level into the group,
matching two single clicks.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +29 / -26  |
| Tests     | +97 / -3   |
…aw#9491)

In order to keep the style panel's standalone dropdown popovers
consistent with the rest of the panel, this PR gives the geo (Shape),
arrow kind (Line), and spline dropdowns the standard popover gap instead
of opening flush against the panel.

These standalone "menu" dropdowns relied on `StylePanelDropdownPicker`'s
default `sideOffset` of `0`, which cancelled the standard 8px popover
gap and jammed their popovers directly against the panel's left edge.
The arrowhead and vertical-align popovers already open with the usual
8px gap, so the Shape/Line/spline dropdowns looked misaligned by
comparison. Changing the default `sideOffset` from `0` to `8` brings
them in line. The inline fill-extra and vertical-align pickers pass an
explicit offset and are unaffected.

### Change type

- [x] `bugfix`

### Test plan

1. Open the examples app and select a geo shape, a line, and an arrow
together so the Shape, Line, Arrows, and Spline dropdown rows all appear
in the style panel.
2. Open each dropdown and verify every popover opens to the left of the
panel with the same small gap (no popover should sit flush against the
panel edge).

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

### Release notes

- Fix the style panel Shape, Line, and spline dropdown popovers so they
open with the same gap as the other dropdowns instead of flush against
the panel.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +5 / -2    |
In order to avoid redundant `store.put` calls when deleting many shapes
at once, this PR moves instance page state cleanup for deleted shape IDs
into the operation-complete handler so bulk deletions update page state
once per operation instead of once per shape.

### Change type

- [x] `improvement`

### Test plan

1. Select many shapes and delete them
2. Confirm selection clears and page state updates only once
3. Undo and confirm shapes and selection are restored

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

### Release notes

- Improve performance of deleting many shapes at once by batching page
state cleanup

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Core code       | +13 / -11  |
| Tests           | +38 / -0   |
In order to prevent malformed asset object names from surfacing as
sync-worker Sentry errors, this PR validates R2 asset keys before
calling R2 and converts Cloudflare invalid object-name errors into 400
responses. This likely happens when an uploaded asset has an extremely
long original filename; before this change, the client included that
filename in the R2 object name, so a 10,000-character filename could
exceed R2 object key limits and trigger `10020`. This PR also caps
generated upload object names so long filenames do not exceed R2 limits.

### Change type

- [x] `bugfix`

### Test plan

1. Upload or request an asset with an object name longer than R2 allows
and confirm the worker returns 400 instead of reporting an internal
error.
2. Open or download a file containing a stale malformed asset URL and
confirm invalid asset object names are skipped instead of reported to
Sentry.

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

### Release notes

- Fix uploaded asset handling for invalid R2 object names.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +48 / -15  |
| Tests     | +106 / -0  |
| Apps      | +13 / -3   |
…ldraw#9483)

Follow-up to tldraw#9472. That PR added `Editor.isMounted` as a plain boolean
that was set to `true` on the `mount` event and never reset — but the
editor's component can unmount without the editor being disposed, so the
flag could get stuck at `true` while the canvas was no longer mounted.

The editor's `mount`/unmount lifecycle is a `<Layout>` (canvas subtree)
concern, decoupled from editor disposal. `<Layout>` can leave the tree
without the editor-creation effect re-running — via the crash path (a
`crashingError` renders `<Crash>` instead of `<Layout>`) or the
`OptionalErrorBoundary` around the canvas catching a render error. In
both cases the editor is not disposed, so `isMounted` stayed `true` even
though nothing was mounted.

This PR makes mount state a real, reactive toggle:

- Adds an `unmount` event to `TLEventMap`, emitted from `useOnMount`'s
teardown, so `mount` is balanced by `unmount`.
- Replaces the `isMounted` field with a reactive `getIsMounted()` backed
by an atom, set `true` on `mount` and `false` on `unmount`. Being
reactive, it can be read inside `computed`/`react`.
- `dispose()` emits `unmount` first (while still mounted and listeners
are attached), so a full unmount — where the editor is disposed before
the canvas teardown runs — still balances `mount` with `unmount` and
leaves `getIsMounted()` reading `false`. The teardown and `dispose()`
guard on the current state so `unmount` fires exactly once regardless of
ordering.

```ts
if (editor.getIsMounted()) runNow()
else editor.once('mount', runNow)

editor.on('unmount', () => {
  // canvas left the tree (crash fallback, error boundary, or disposal)
})
```

### API changes

- Added the `unmount` event to `TLEventMap`.
- Reworked the unreleased `Editor.isMounted` field (added in tldraw#9472, not
in any release) into `Editor.getIsMounted()`, a reactive getter that
returns `false` after unmount as well as before mount.

### Change type

- [x] `api`

### Test plan

1. Mount a `<Tldraw>` / `TldrawEditor`; `editor.getIsMounted()` is
`true` in `onMount` and after.
2. Unmount the component; `editor.getIsMounted()` is `false` and an
`unmount` event fires.
3. A `react`/`computed` reading `editor.getIsMounted()` recomputes
across mount/unmount.

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

### Release notes

- Add an `unmount` event and make `Editor.getIsMounted()` a reactive
value that tracks whether the editor's component is currently mounted.

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Core code       | +33 / -6   |
| Tests           | +26 / -0   |
| Automated files | +3 / -1    |
tldraw#9509)

Fixes how the style panel highlights the active fill when the current
fill lives in the overflow dropdown.

Two related changes:

- **Don't highlight a dropdown item when the current value isn't in the
dropdown.** Previously, hovering the rightmost fill could pre-select a
fill option before one was chosen. Items now only show as active when
the current value actually belongs to that dropdown (`valueInItems`).
- **Highlight the overflow dropdown trigger when its active item is
selected.** The overflow dropdown's trigger button now shows as active
when the current value is one of the items inside it, via a new
`isOverflow` prop on `StylePanelDropdownPicker`.

Closes tldraw#9339

### Change type

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

### Test plan

1. Create a shape and select a light fill (2nd or 3rd in the list).
2. Click the 4th fill icon.
3. None of the previous 3 fills should stay selected — all should be
blank.
4. Select a fill that lives in the overflow dropdown and confirm the
overflow trigger shows as active.

### API changes

- Added optional `isOverflow` prop to `StylePanelDropdownPickerProps` —
marks a dropdown as the overflow of another radio group so its trigger
shows active when the group's active item is inside it.

---------

Co-authored-by: Steve Ruiz <[email protected]>
In order to prevent arrow handle double-clicks from stealing a second
click that is becoming a drag, this PR defers handle double-click
actions until pointer up.

### Change type

- [x] `bugfix`

### Test plan

1. Double-click an arrow endpoint handle and confirm the arrowhead
toggles on release.
2. Click an arrow endpoint handle, then click again and drag before
releasing; confirm the handle drags and the arrowhead does not toggle.

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

### Release notes

- Fix arrow endpoint double-click toggles firing before a second click
can become a drag.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +14 / -2   |
| Tests     | +67 / -0   |
…draw#9492)

In order to make shape hover and hit-testing feel right across input
types, this PR makes the hit-test margin coarse-pointer aware. Fine
pointers (mouse/trackpad) now use a tighter default margin, and coarse
pointers (touch/pen) use a slightly larger one so imprecise input still
lands on shapes. The margin calculation is centralized behind a new
`Editor.getHitTestMargin()` computed getter that every hit-testing call
site (hover, select, erase, crop) now shares. It's derived from the
efficient zoom level, so the margin stays a constant distance in screen
space at any zoom.

Specifically:

- Adds a `coarseHitTestMargin` option (default `4`), used when the
pointer is coarse.
- Retunes the default `hitTestMargin` from `8` to `3` for fine pointers.
- Adds `Editor.getHitTestMargin()` and routes all call sites through it,
removing the duplicated `options.hitTestMargin / zoomLevel` expression.

### Change type

- [x] `improvement`

### Test plan

1. With a mouse/trackpad, hover just outside a shape — you now need to
be within ~3px of its edge/fill.
2. On a touch device or with a pen (coarse pointer), the margin is ~4px.
3. Zoom in and out; the hover/hit distance stays constant in screen
space.

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

### Release notes

- Make the shape hit-test margin pointer-aware: fine pointers
(mouse/trackpad) use a tighter default of 3px and coarse pointers
(touch/pen) use 4px. Configurable via the `hitTestMargin` and new
`coarseHitTestMargin` options.

### API changes

- Added `Editor.getHitTestMargin()`, returning the current hit-test
margin in page space (coarse-pointer aware and zoom-adjusted).
- Added the `coarseHitTestMargin` option (default `4`).
- Changed the default `hitTestMargin` from `8` to `3`.

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Core code       | +35 / -22  |
| Automated files | +5 / -1    |
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Large CI and dotcom deploy changes (Supabase preview DBs, Fly multinode Zero, consolidated npm publish) affect production release and staging paths; wrong secrets or workflow behavior could break deploys or publishes even though this is framed as an upstream sync.

Overview
This PR syncs the fork with upstream tldraw v5.2.3—mostly repo hygiene, CI, and release/deploy plumbing rather than a single product feature.

Agent and editor tooling centralizes skills under skills/ with symlinks for .agents, .claude, and .cursor, adds AGENTS.md as the main agent guide (replacing the large CONTEXT.md / duplicated CLAUDE.md content), trims old Cursor rules and the Notion feature-page command, and adds Claude launch configs plus a Cursor marketplace entry for the MCP plugin.

Lint and format moves toward oxlint (.oxlintrc.json) and oxfmt (.oxfmtrc.json), updates pre-commit to run install/build-api/i18n only when relevant files change, and shifts Claude’s formatter hook from per-edit to on Stop.

GitHub Actions is a broad refresh: Node 24.17, ARM runners, node_modules / build-tool / Playwright caching, check-circular-deps, and shared setup-playwright / Discord fail-notify actions. SDK publishing is merged into a single publish.yml (canary/next/patch/manual/new/internal) with downstream notifies to tldraw-desktop and tldraw-internal; several older publish workflows are removed. New or updated automation includes MCP app deploy, issue triage and release-notes Claude workflows, Framer rewrites PR bot, and on-demand device e2e (Appium iOS/Android). dotcom deploy drops Neon preview branches and AWS/SST Zero paths in favor of Supabase branch CLI for PR DBs and flyio / flyio-multinode Zero targets, with new deploy secrets (Plain, user content, Zero R2/OTEL, etc.). Playwright jobs gain path filters, prebuilt examples/dotcom client, and a new S3 report bucket hostname.

Community and docs: CONTRIBUTING.md states contributions/PRs are not accepted; README and RELEASES.md reflect v5-style docs/licensing and releases from production via publish.yml. Issue templates are simplified (typed issues, Discord contact link).

Misc: removes the api-extractor .d.mts patch, adds Yarn npmMinimalAgeGate, and expands .gitignore for agent/pr-walkthrough artifacts.

Reviewed by Cursor Bugbot for commit 4df03f3. Bugbot is set up for automated code reviews on this repo. Configure here.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednext@​15.4.6 ⏵ 15.5.1952 -13100 +7591 +199 +4970
Updated@​tiptap/​extension-font-family@​3.6.2 ⏵ 3.13.010010069 -31100100
Added@​types/​topojson-client@​3.1.51001007080100
Added@​types/​topojson-specification@​1.0.51001007080100
Addedovsx@​0.10.798100909470
Updated@​rocicorp/​zero@​0.19.2025050203 ⏵ 1.7.071 -2310091 +8100 +1100
Added@​types/​better-sqlite3@​7.6.131001007180100
Added@​typescript/​native-preview@​7.0.0-dev.20260129.110010071100100
Updated@​jest/​expect-utils@​30.0.5 ⏵ 30.4.1100 +110072 +191100
Added@​types/​rbush@​3.0.41001007280100
Added@​formatjs/​unplugin@​1.1.37310010092100
Updated@​types/​pg@​8.11.11 ⏵ 8.16.0100 +110073 +183100
Updatedesbuild@​0.25.4 ⏵ 0.28.091 +110073 +190100
Added@​wdio/​cli@​9.27.2981007496100
Added@​types/​adm-zip@​0.5.71001007481100
Updated@​tiptap/​pm@​3.6.2 ⏵ 3.13.0991007498 -1100
Updatedintl-messageformat@​10.7.7 ⏵ 11.1.210010074 -197100
Updated@​tiptap/​extension-code@​3.6.2 ⏵ 3.13.0100 +110074 +198 -1100
Addedmermaid@​11.15.07510010095100
Added@​dimforge/​rapier2d-compat@​0.19.3821007587100
Updated@​tiptap/​extension-highlight@​3.6.2 ⏵ 3.13.0100 +110075 +1100100
Updated@​types/​react-dom@​18.3.7 ⏵ 19.2.31001007585100
Updated@​vitest/​ui@​3.2.4 ⏵ 4.1.799 +110075 -698100
Updated@​tiptap/​starter-kit@​3.6.2 ⏵ 3.13.0991007598 -1100
Addedtopojson-client@​3.1.010010010075100
Added@​types/​d3-geo@​3.1.01001007680100
Updatedreact-intl@​6.8.9 ⏵ 8.1.3100 +110076 +198100
Addedopen-cli@​8.0.01001007781100
Updatedjest-matcher-utils@​30.0.5 ⏵ 30.4.1100 +110077 +191100
Addedd3-geo@​3.1.11001007882100
Updated@​clerk/​clerk-react@​5.22.8 ⏵ 5.61.679 -21100 +1694 +197 -3100
Updated@​tiptap/​react@​3.6.2 ⏵ 3.13.099 +110079 +198 -1100
Updated@​types/​react@​18.3.23 ⏵ 19.2.7100 +11007995100
See 69 more rows in the dashboard

View full report

@socket-security

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @vercel/fun is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/[email protected]npm/@vercel/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@vercel/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm @zip.js/zip.js is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/@wdio/[email protected]npm/[email protected]npm/@wdio/[email protected]npm/@wdio/[email protected]npm/@wdio/[email protected]npm/@zip.js/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@zip.js/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm better-sqlite3 is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: templates/simple-server-example/package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm cheerio is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: apps/vscode/extension/package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm css-tree is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm data-urls is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm edgedriver is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/@wdio/[email protected]npm/[email protected]npm/@wdio/[email protected]npm/@wdio/[email protected]npm/@wdio/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm es-module-lexer is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/[email protected]npm/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm es-module-lexer is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/[email protected]npm/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm execa is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/@wdio/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm googleapis is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/@google/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm htmlparser2 is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm js-yaml is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/[email protected]npm/@wdio/[email protected]npm/@lingual/[email protected]npm/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm jsdom is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm jsdom is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm mermaid is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: apps/examples/package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm mermaid is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: apps/examples/package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm mermaid is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: apps/examples/package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm next is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: apps/docs/package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm rimraf is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/@rocicorp/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm robust-predicates is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm spacetrim is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/@wdio/[email protected]npm/[email protected]npm/@wdio/[email protected]npm/@wdio/[email protected]npm/@wdio/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm tar is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: packages/create-tldraw/package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm underscore is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/[email protected]npm/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm webdriverio is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: apps/examples/package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm webpack is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: apps/vscode/extension/package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm workerpool is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/@wdio/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Network access: npm @apidevtools/json-schema-ref-parser in module globalThis["fetch"]

Module: globalThis["fetch"]

Location: Package overview

From: ?npm/[email protected]npm/@apidevtools/[email protected]

ℹ Read more on: This package | This alert | What is network access?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should remove all network access that is functionally unnecessary. Consumers should audit network access to ensure legitimate use.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@apidevtools/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

See 187 more rows in the dashboard

View full report

}

function sanitizeSvgInner(svgText: string, depth: number): string {
const doc = new DOMParser().parseFromString(svgText, 'image/svg+xml')
// eslint-disable-next-line no-console
console.log('Publishing to Open VSX...')
// OVSX_PAT is read from environment variable by ovsx CLI
await execAsync(`npx ovsx publish${preRelease ? ' --pre-release' : ''} ${vsixPath}`)

@oscar-luma oscar-luma left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kaznaan
kaznaan merged commit 81ab9e2 into main Jul 6, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.