Skip to content

[pull] main from tldraw:main#580

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

[pull] main from tldraw:main#580
pull[bot] merged 7 commits into
code:mainfrom
tldraw:main

Conversation

@pull

@pull pull Bot commented Jun 9, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


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

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

MitjaBezensek and others added 7 commits June 9, 2026 11:04
…numbers (#9076)

In order to stop two concurrent PRs from each picking the same
zero-cache migration number and silently colliding on `main`, this PR
adds a check that validates the migration filenames in
`apps/dotcom/zero-cache/migrations/`. Migration numbers are chosen by
hand and the filenames differ, so git merges two same-numbered
migrations cleanly and `main` ends up with a duplicate. The check throws
on duplicate or non-contiguous numbers and runs as a vitest test under
`yarn test-ci`. Because CI checks out the post-merge tree (both
`pull_request` and the `merge_group` queue), the collision is caught
when the second PR is merged rather than after it lands. Closes #9075.

The existing duplicate `027` on `main` is grandfathered in via
`GRANDFATHERED_DUPLICATE_NUMBERS` so the check passes today; new
migrations must use a unique, contiguous number.

### Change type

- [x] `other` (CI/tooling, test)

### Test plan

1. From `apps/dotcom/zero-cache`, run `yarn test run` — passes against
the real migrations directory.
2. Create a colliding file: `cp migrations/031_group_id_indexes.sql
migrations/031_dupe.sql`, rerun — fails with "Duplicate migration number
031". Remove it.
3. Rename a file to create a gap: `mv migrations/031_*.sql
migrations/033_*.sql`, rerun — fails with "missing 032". Revert.

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

### Code changes

| Section        | LOC change |
| -------------- | ---------- |
| Tests          | +43 / -0   |
| Apps           | +64 / -0   |
| Config/tooling | +12 / -0   |
…#9064)

In order to keep the sidebar anchored to the group you're working in,
this PR changes file deletion to stay within the current group.
Previously, deleting a file from a group navigated to the top of "My
files", which scrolled the sidebar to the top and opened an unrelated
personal file. Now we navigate to the top of the deleted file's group,
falling back to "My files" only when the group has no remaining files.
Closes #8974.

### How it works

The sidebar has no direct "scroll to group" behavior — scrolling is a
side effect of which file is active. Each file link calls
`scrollIntoView({ block: 'nearest' })` when it becomes the active file
(the one in the URL). Previously, deleting a group file navigated to the
top of "My files", so that link became active and scrolled the sidebar
up and out of the group. Navigating to the group's top remaining file
instead makes a file *inside the group* active, so the sidebar reveals
it in place and stays anchored to the group.

A couple of details for reviewers unfamiliar with this code:

- `block: 'nearest'` means "top of current group" is really "reveal the
group's top file" with minimal scrolling — it matches the rest of the
sidebar's active-file navigation rather than pinning the group header to
the top of the viewport.
- `ensureFileVisibleInSidebar` is load-bearing: it expands the group so
the active link exists in the DOM before we navigate. Without it the
scroll would no-op when the group is collapsed (for example, deleting
from the editor's file menu while the sidebar group is closed).

### Change type

- [x] `bugfix`

### Test plan

1. Enable groups and create a group with two or more files.
2. Open one of the files in the group.
3. Delete a file from the group.
4. The sidebar should stay scrolled to the current group and open the
top remaining file in that group, rather than jumping up to "My files".
5. Delete the last file in the group — navigation should fall back to
"My files" (creating a new file if there are none).

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

### Release notes

- Fix the sidebar jumping to "My files" when deleting a file from a
group; it now stays in the current group.

### Code changes

| Section | LOC change |
| ------- | ---------- |
| Tests   | +45 / -0   |
| Apps    | +19 / -7   |
…pabase (#9033)

In order to stop Zero from doing a full reset of its server replica and
every connected client on every Supabase schema change, this PR adopts
Zero's manual schema-change hook.

tldraw.com runs Zero with upstream Postgres on Supabase. Zero detects
schema changes via Postgres event triggers, but Supabase doesn't fire
them for `ALTER PUBLICATION`. An undetected schema change makes Zero do
a full reset of its server replica and every connected client —
increasingly expensive as the DB grows.

- `migrate.ts` calls `zero_0.update_schemas()` once after all migrations
apply, in the same transaction, so schema changes update the replica in
place. Guarded so it's a no-op before Zero has booted (fresh DB). Uses
`to_regprocedure` to resolve the no-arg overload (`zero_0` has two
`update_schemas` overloads; `to_regproc` returns NULL for the ambiguous
name and would silently skip the call).
- A drift check fails the migration loudly if the shard schema is ever
not `zero_0`, so the hardcoded name can't silently stop matching.
- `032_enable_ddl_detection.sql` sets `ddlDetection = true` (guarded) as
a safety-net; it's already `true` on staging.

Verified on staging: shard schema is `zero_0`, the no-arg overload
exists, and `ddlDetection` is already `true`.

This migration is numbered `032` (the next free number on `main`). #9029
also currently uses `032` for
`032_rename_group_admin_role_to_member.sql`; whichever PR lands second
will need to bump its migration number to avoid the collision.

Closes #9031

### Change type

- [x] `other`

### Test plan

1. Deploy to staging and apply a migration that contains DDL.
2. Confirm Zero updates its replica in place — no full reset, clients
don't re-download all state (check Zero logs).
3. Confirm `SELECT "ddlDetection" FROM zero_0."shardConfig";` is `true`
and `update_schemas()` ran without error.
Splits the vitest 4 upgrade out of the Dependabot group bump (#8998),
where it was bundled with low-risk patches. vitest 3 → 4 is a major bump
that needs real test changes, so it gets its own PR.

### What changed

- Bump `vitest`, `@vitest/ui`, `@vitest/coverage-v8` from `^3.2.4` to
`^4.1.7`. (4.1.8 exists but is blocked by the repo's 7-day
`npmMinimalAgeGate`; `^4.1.7` is the newest version that passes the
gate, and caret will pick up 4.1.8 once it ages in.)

### vitest 4 breakages fixed

- **Constructor mocks** — vitest 4 won't let an arrow-function or
`mockReturnValue` mock be called with `new`. Converted the `Range`,
`FontFace`, and `ClipboardItem` global mocks to function-based mocks
(`TextManager`, `FontManager`, `clipboardCallbacks` tests).
- **`vi.restoreAllMocks()`** now clears a factory mock's implementation,
so `devFreeze`'s dev-mode block explicitly mocks `isDev → true`.
- **Web storage** — vitest 4's jsdom no longer exposes
`localStorage`/`sessionStorage`. Added an in-memory polyfill to the
shared vitest setup (`internal/config/vitest/setup.ts`) so
storage-touching code works (fixes `LocalIndexedDb`).
- **JSX transform** — vite 8 transforms with oxc and ignores the
deprecated `esbuild.jsx` option. The dotcom client keeps `jsx: preserve`
in its tsconfig for the app build, so its vitest config now uses
`@vitejs/plugin-react`.

### Verification

All package and app test suites pass locally under vitest 4 (editor,
tldraw, store, sync-core, utils, dotcom/client, etc.).

Note: `apps/dotcom/sync-worker`'s `replicatorMigrations` snapshot
differs locally only because node 26's bundled SQLite emits an extra
`sqlite_stat4` table; CI runs node 24, which matches the committed
snapshot, so it's left unchanged.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use shift + Q as a shortcut to select the style form a shape and have it
be applied to the next shape.

Have the same base behavior that
#8917 initially introduced to allow
you to capture the style of a shape on hover, but rather than apply it
directly to an existing shape if you have one selected it should just
apply the style to the next shape.

### API changes

- added a `shift+q` shortcut to copy styles form a hovered shape in
order to apply it for the next shape

### Change type

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

### Test plan

<breathing intensifies>

1. Create a shape using any colour
2. Create an other shape using any other colour
3. Hover first shape
4. press `shift+q`
5. create an other shape

The shape created should now have the same style than the first shape

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

### Release notes

- Added new shortcuts in order to copy styles from a selected shape.
`shift+q` will copy the styles of a hovered shape and set it for the
next shape to be created
In order to pick up Zero 1.6's deployment and reliability fixes, this PR
bumps `@rocicorp/zero` from `1.5.0` to `1.6.1` across the dotcom client,
sync-worker, zero-cache, and `dotcom-shared`. Zero 1.6 ships with no
breaking changes versus 1.5.

The Fly/Docker deploy templates read the version from
`zero-cache/package.json` via `internal/scripts/deploy-dotcom.ts`, so
the bump propagates to the deployed zero-cache image automatically.

### Change type

- [x] `improvement`

### Test plan

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

### Release notes

- Bump Zero to 1.6.1.
Closes #9024

In order to stop the default Google Maps embed from reading
`process.env.NEXT_PUBLIC_GC_API_KEY` inside the SDK (#9024), this PR
lets consumers pass per-embed config through `EmbedShapeUtil.configure`.
Config is keyed by embed type, so each definition only receives its own
values and no key leaks across embeds.

```ts
EmbedShapeUtil.configure({
	embedConfig: { google_maps: { apiKey: '...' } },
})
```

tldraw.com now configures the embed util explicitly (reading the
vite-inlined key in app code) instead of relying on the SDK reading the
env var.

### Change type

- [x] `api`

### Test plan

1. Configure `EmbedShapeUtil` with a `google_maps` api key and paste a
Google Maps URL — the embed renders with the key.
2. Without config, the maps embed URL has no key (no env fallback in the
SDK).

- [x] Unit tests

### API changes

- Added `embedConfig` option to `EmbedShapeOptions`.
- Added `GoogleMapsEmbedConfig` and `DefaultEmbedConfig` types.
- `EmbedDefinition`/`CustomEmbedDefinition` are now generic over a
`Config`; `toEmbedUrl(url, config?)` takes an optional per-embed config.
- `getEmbedInfo`/`matchUrl` accept an optional `embedConfig` argument.

### Release notes

- Add `embedConfig` to `EmbedShapeUtil` so embeds like Google Maps can
be given an API key via `EmbedShapeUtil.configure({ embedConfig: {
google_maps: { apiKey } } })`. The default Google Maps embed no longer
reads `process.env.NEXT_PUBLIC_GC_API_KEY`.
@pull pull Bot locked and limited conversation to collaborators Jun 9, 2026
@pull pull Bot added the ⤵️ pull label Jun 9, 2026
@pull
pull Bot merged commit 2973e52 into code:main Jun 9, 2026
@pull
pull Bot had a problem deploying to deploy-production June 9, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to bemo-canary June 9, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to npm deploy June 9, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to bemo-canary June 9, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to deploy-staging June 9, 2026 15:13 Error
@pull
pull Bot had a problem deploying to vsce publish June 9, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to deploy-staging June 9, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to npm deploy June 9, 2026 15:13 Failure
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants