Sync/upstream main 2026 07 06#16
Conversation
…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`
…#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> <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> </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 |
…tldraw#9010) Reverts tldraw#8943 (don't wanna ship this yet while pending discussion tldraw#8915)
…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
…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 |
PR SummaryHigh Risk Overview Agent and editor tooling centralizes skills under Lint and format moves toward oxlint ( GitHub Actions is a broad refresh: Node 24.17, ARM runners, Community and docs: Misc: removes the api-extractor Reviewed by Cursor Bugbot for commit 4df03f3. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
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.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||
|
|
||
| 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}`) |
chore: sync main with upstream tldraw v5.2.3