Skip to content

[pull] main from tldraw:main#604

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

[pull] main from tldraw:main#604
pull[bot] merged 16 commits into
code:mainfrom
tldraw:main

Conversation

@pull

@pull pull Bot commented Jun 18, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


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

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

mimecuvalo and others added 16 commits June 18, 2026 09:30
…8905)

Adjusting a theme slider in the `custom-theme` example passed a
brand-new `themes` object to `<Tldraw>` on every change. `useLocalStore`
treats `themes` as part of the store options, so each new reference
re-ran `createTLStore` and reloaded from persistence — the whole board
visibly flashed on every slider drag.

This reworks the example to apply theme changes through the programmatic
API instead of the prop:

- The `themes` prop is now a stable, module-level object passed once to
`<Tldraw>`. It registers the custom `pink` color and custom fonts at
store creation and seeds the starting values. Because its reference
never changes, the store is never recreated.
- Slider adjustments are applied at runtime with `editor.updateTheme({
...editor.getTheme('default')!, fontSize, lineHeight, strokeWidth })`,
spreading the current theme so custom colors and fonts are preserved.
Shapes re-render reactively without reloading the canvas.

No SDK changes — the fix is entirely in the example.

### Change type

- [x] `bugfix`

### Test plan

1. Run `yarn dev` and open http://localhost:5420/custom-theme.
2. Adjust any of the sliders (font size, line height, stroke width).
3. The shapes re-render with the new theme values without the canvas
flashing or reloading.

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

### Release notes

- Update the `custom-theme` example to apply runtime theme changes via
`editor.updateTheme()` so adjusting theme display values no longer
reloads the canvas.
…ns (#9279)

In order to correct one-off bad machine translations without waiting for
a full re-translation cycle, this PR adds an `i18n-fix-strings` script
that patches specific (key, locale) translations directly in Lokalise.
The automated i18n pipeline (`i18n-upload-strings` /
`i18n-download-strings`) occasionally returns a wrong translation — a
word translated in the wrong sense, or scrambled placeholders/tags — and
there was no targeted way to correct just those entries.

The script is data-driven (a `FIXES` table of key → English source →
per-locale corrected value), dry-runs by default, and only writes when
passed `--apply`. It refuses any fix that would drop a placeholder or
HTML tag present in the English source, and normalizes locale codes so
`ko_KR` matches `ko-kr`. It targets the dotcom Lokalise project via the
existing `LOKALISE_PROJECT_ID` / `LOKALISE_API_TOKEN` env vars.

Usage:

```bash
yarn i18n-fix-strings            # dry run — prints the before/after diff per locale
yarn i18n-fix-strings --apply    # write the corrections to Lokalise
```

The script currently carries one correction: French "Clear search" was
`Recherche propre` ("clean search", adjective) instead of the verb
`Effacer la recherche`. This has already been applied to Lokalise; it
ships in the table as documentation and a re-runnable record.

### Change type

- [x] `other`

### Test plan

1. Run `yarn i18n-fix-strings` (dry run) with valid
`LOKALISE_PROJECT_ID` / `LOKALISE_API_TOKEN` — confirm it prints the
before/after diff and writes nothing.
2. Run with `--apply` — confirm the targeted translation is updated in
Lokalise and a re-run reports "already correct".

### Code changes

| Section        | LOC change |
| -------------- | ---------- |
| Config/tooling | +195 / -0  |
## What

Fixes a cluster of "My files" bugs in the workspaces model that all
trace to one root cause.

A shared file you open as a guest is mirrored into your home group —
this is the intended **guest file** feature (the file shows in "My
files" with a guest badge so you can find it again). But if that file is
owned by a workspace you're a **member of**, it would show in "My files"
yet open into that workspace — because the active workspace is derived
from the file's `owningGroupId`. So clicking it (or switching to "My
files", which opens the top file) bounced you into the workspace, and
you could never get back to your home.

This happens when:

- you open a workspace's shared file as a guest, then later **join**
that workspace, or
- a workspace's **welcome file** is mirrored into home during the
create-workspace race.

It explains the reported symptoms: shared files and a "Welcome to your
workspace" file showing in "My files", and clicking them teleporting the
user into another workspace with no way back.

## Preview

https://pr-9270-preview-deploy.tldraw.com/

## Fixes

1. **Teleport / "can't get back to My files" — read-side filter
(`getWorkspaceFilesSorted`).** For the home workspace, the sidebar now
lists only files it owns, legacy files, and genuine guest files (owned
by workspaces you're **not** a member of). A file owned by a workspace
you **are** a member of is no longer listed in home, so it can't bounce
you out. Guest-file mirroring (`onEnterFile`) is unchanged — guest files
still appear as before. Filtering on read also fixes already-affected
users on deploy, before any data cleanup.

2. **File-count limit consistency (`canCreateNewFile`).** It now counts
only files the workspace actually owns (`owningGroupId ===
workspaceId`), not guest files or hidden/mislinked rows — matching the
server's `assertNotMaxFiles`. Prevents "max files reached" firing on
files the user can't see or remove.

3. **Empty home stays switchable (`createFile`).** Authorizes the home
workspace via `getRole`/`can('addFiles')` instead of a raw `group_user`
lookup. The home workspace (`id === userId`) is implicitly owned and may
have no `group_user` row (e.g. before it syncs), so creating a file
there could be rejected — leaving an empty home impossible to switch
back to. This matches how every other workspace op already authorizes.

## How to verify

On the preview (https://pr-9270-preview-deploy.tldraw.com/), with two
accounts:

1. **Account A:** create a workspace, add a file inside it, then copy
that **file's share link** and the **workspace invite link**.
2. **Account B (incognito):** open the **file share link** → the file
appears in "My files" with a guest badge.
3. **Account B:** open the **workspace invite link** and accept it
(you're now a member of the workspace).
4. **Account B:** click **"My files"** → you stay in "My files".

Before the fix, step 4 bounced you into the workspace (you couldn't get
back to "My files").

## Testing

- New scenario test — `opening a guest file does not trap the member
outside their home after they join its workspace`
(`sharing-live.scenario.spec.ts`). It fails on the unfixed code (the
member is teleported into the workspace) and passes here.
- Unit tests in `mutators.test.ts` cover `createFile` home authorization
and `onEnterFile` guest / member / legacy mirroring.
- `yarn typecheck`, lint, and the full dotcom e2e suite pass.
In order to stop burning GitHub Actions minutes when the dotcom dev
stack fails to start, this PR reduces the Playwright `webServer` startup
timeout in CI from `840_000`ms (14 min) to `180_000`ms (3 min). That
timeout is how long Playwright waits for `http://localhost:3000` to
respond before any test runs; on a stuck or broken stack it previously
sat idle for ~14 minutes before failing. The dotcom server should boot
well within 3 minutes, so this aborts a hung run quickly instead.

The old value was derived to cover the dev stack's internal per-stage
readiness budgets in `zero-cache/dev-env.ts` (~13 min worst case). Those
internal budgets are left unchanged: they gate individual stages
(Postgres, migrations, Zero cache, sync worker) and govern the local dev
experience, and a genuinely failing stage already throws and exits
quickly. The 3-min cap now bounds the whole `yarn dev-app` command in
CI. Trade-off: if a CI cold start ever legitimately needs more than 3
minutes, the e2e job will fail at that mark — the fix then is to bump
this value to a measured number, not restore 14 minutes.

### Change type

- [x] `improvement`

### Test plan

- A healthy CI e2e run should start tests exactly as before.
- A run where the dev server never comes up should now abort at ~3 min
instead of ~14 min.

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

### Code changes

| Section        | LOC change |
| -------------- | ---------- |
| Config/tooling | +5 / -2    |
)

In order to keep nested lists from collapsing into a single column so
quickly, this PR halves the default per-level indentation on rich-text
lists. Closes #9041. Closes #8712

List indentation is set on `.tl-rich-text ul/ol` in
`packages/editor/editor.css`, a single rule shared by all rich text
(note shapes, text shapes, geo labels), so the reduction applies
everywhere lists render. The base `padding-left` drops from `3.25ch` to
`1.625ch`. The ordered-list variants that add room for 2- and 3-digit
numbers keep their `+1ch` / `+2ch` digit-width increments, so numbered
lists stay aligned: `4.25ch` → `2.625ch` and `5.25ch` → `3.625ch`.

### Change type

- [x] `improvement`

### Test plan

1. Run `yarn dev` and create a note shape.
2. Type a bulleted list and press Tab to nest several levels; confirm
each level indents about half as much as before and bullets stay
readable.
3. Repeat with an ordered list, including lists long enough to reach 10+
and 100+ items, and confirm numbers remain aligned.
4. Spot-check a text shape and a geo shape label to confirm rich-text
lists still render correctly.

### Release notes

- Reduce the default indentation on rich-text lists so nested lists take
up less horizontal space.

---------

Co-authored-by: huppy-bot[bot] <128400622+huppy-bot[bot]@users.noreply.github.com>
)

In order to tighten up the tldraw.com UI, this PR makes a batch of
focused refinements to the dotcom app shell: it standardizes the SVG
icons to use \`currentColor\`, slims down the custom theme token set,
and reworks the invite, sign-in, and workspace settings dialogs along
with sidebar and menu spacing.

Highlights:

- **Icons** — \`edit\`, \`search\`, \`settings\` icons now use
\`currentColor\` instead of hard-coded fills so they inherit theme text
color; add an \`avatar\` icon.
- **Theme tokens** — strip exported \`tla\` theme variants down to the
keys that don't resolve through shared \`tl-color-*\` tokens, removing
~hundreds of redundant per-theme overrides in \`ui-themes.ts\`.
- **Invite dialog** — retitle to "Join workspace", move the workspace
name into a description line, drop the logo image and the redundant "No
thanks" button, and rename the primary action to "Accept invitation".
- **Sign-in dialog** — simplify the create-account copy.
- **Workspace settings dialog** — layout and structure cleanup.
- **Sidebar & menus** — spacing and CSS adjustments across the sidebar,
top bar, share menu, buttons, and dialogs.

Relates to #9082.

### Change type

- [x] `improvement`

### Test plan

1. Run \`yarn dev-app\` and open tldraw.com locally.
2. Open the invite flow and confirm the dialog shows "Join workspace"
with the workspace name and a single "Accept invitation" button.
3. Open the sign-in dialog and confirm the create-account copy.
4. Open workspace settings and confirm the dialog layout.
5. Switch between custom themes (e.g. solarized) in light and dark and
confirm colors still apply correctly.
6. Confirm the edit, search, and settings icons follow the current text
color.

### Release notes

- Polish the tldraw.com invite, sign-in, and workspace settings dialogs,
sidebar, and theming.

### Code changes

| Section        | LOC change  |
| -------------- | ----------- |
| Apps           | +351 / -854 |
| Config/tooling | +4 / -1     |

---------

Co-authored-by: Mime Čuvalo <mimecuvalo@gmail.com>
#9278)

Closes #8328 

The fix in #8334 surfaced a rather
unpleasant visual issue that occur once the user tabulate and press
enter: the "back to canvas" stays visible during the animate.

This PR fixes that by forcing the button to hide once the "focus" button
is pressed.

### Change type

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

### Test plan

1. Create a shape
2. Go far far, really far (on the left, on the right, where you want,
you do you)
3. Press tab
4. Press enter

=> The "back to content" button is not visible during the animation

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

### Release notes

- Small visual improvement with the "focus canvas" a11y behaviour, we
hide the "back to canvas" button during the animation that pan back to
the first created shape
In order to stop common English words and phrases from being rendered as
broken mermaid diagrams on tldraw.com, this PR tightens the mermaid
detection heuristic in `simpleMermaidStringTest`. Pasting text like
"timeline", "kanban-board", "info", or "graph paper" previously matched
a mermaid diagram keyword as a bare prefix and was routed to the mermaid
renderer instead of being inserted as a plain text shape (closes #8481).

The detection regex matched diagram keywords with no word boundary and
no structural validation, so:

1. **No word boundary** — "kanban-board" matched because "kanban" is a
prefix.
2. **No structural validation** — a single word like "timeline", or a
phrase like "graph paper", matched even though a real diagram has
structure beyond the bare keyword.

This PR addresses both:

- Adds a trailing `(?![\w-])` word-boundary lookahead so keywords glued
to more text no longer match: "kanban-board", "gantt-chart",
"information", "graphql".
- Requires evidence of an actual diagram for unfenced text — either a
recognized variant suffix (e.g. `sankey-beta`, `stateDiagram-v2`) or a
line break followed by real content. This rejects single-line prose such
as "graph paper is nice" or "pie in the sky" while still accepting
`graph LR\n A --> B`.
- Keeps explicit ` ```mermaid ` fences permissive: a bare keyword inside
a fence the user has labelled as mermaid is still detected, since intent
is clear.

### Change type

- [x] `bugfix`

### Test plan

1. Go to tldraw.com.
2. Copy a common word or phrase ("timeline", "kanban-board", "info",
"graph paper") and paste it onto the canvas — it should appear as a
plain text shape, not a diagram.
3. Paste a real diagram (e.g. `timeline` followed by ` title History`,
or a ` ```mermaid ` fenced diagram) — it should still render as a
mermaid diagram.

- [x] Unit tests

### Release notes

- Fix mermaid detection on tldraw.com so common words and phrases like
"timeline", "kanban-board", "info", and "graph paper" paste as plain
text instead of being rendered as broken diagrams.

### Code changes

| Section | LOC change |
| ------- | ---------- |
| Apps    | +27 / -2   |
| Tests   | +67 / -9   |
In order to make the line "bend" threshold feel consistent at every zoom
level, this PR divides `MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES`
by the current zoom level so the 2px minimum distance between
shift-clicked line handles is measured in screen space rather than page
space. Closes #7661.

Previously the threshold was compared directly against page-space
distances. At low zoom 2 page pixels was effectively invisible (very
easy to trigger), and at high zoom it felt large (hard to add points
close together). This mirrors the zoom-aware drag detection in
`Editor.ts`.

### Change type

- [x] `bugfix`

### Test plan

1. Open the line tool with tool lock enabled.
2. Zoom in (e.g. 400%), draw a line, then shift-click a few screen
pixels past the end handle — a new point should be added.
3. Zoom out (e.g. 25%), draw a line, then shift-click within ~2 screen
pixels of the end handle — the click should merge into the existing
handle instead of adding a point.

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

### Release notes

- Fix the line tool's minimum bend distance so it's measured in screen
pixels, keeping shift-click behavior consistent across zoom levels.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +7 / -2    |
| Tests     | +41 / -0   |
This PR updates the i18n strings.

### Change type
- [x] `other`

Co-authored-by: huppy-bot[bot] <128400622+huppy-bot[bot]@users.noreply.github.com>
Co-authored-by: Mime Čuvalo <mimecuvalo@gmail.com>
…9292)

In order to make file and workspace deletion less ambiguous, this PR
includes the name of the item being deleted in the confirmation dialogs.
Previously the dialogs read generic copy like "Are you sure you want to
delete this file?", which gave no confirmation of which file or
workspace was about to be deleted.

The file delete/forget dialog now reads "Are you sure you want to delete
{fileName}?" (and the "forget" variant for non-owners), and the
delete-workspace confirmation reads "Are you sure you want to delete
{workspaceName}? This action cannot be undone." Unnamed workspaces fall
back to the existing name placeholder. This matches the existing pattern
already used when removing a workspace member.

### Change type

- [x] `improvement`

### Test plan

1. Open a file's menu and click Delete (or Forget as a non-owner);
confirm the dialog names the file.
2. Open a workspace's settings and click Delete workspace; confirm the
dialog names the workspace.

### Release notes

- Show the file or workspace name in delete confirmation dialogs on
tldraw.com.
In order to let our published packages depend on modern ESM-only modules
without breaking CommonJS consumers, this PR drops support for the
now-EOL Node 20 and requires Node `>=22.12.0` — a version where
`require()` of an ES module works natively.

Calling `require()` on an ES module (instead of throwing
`ERR_REQUIRE_ESM`) is unflagged as of Node 22.12.0. Node 20 reached EOL
in April 2026, so rather than carry the awkward `^20.19.0 || >=22.12.0`
range we just require `>=22.12.0`. With that floor, an ESM-only
dependency in a `dist-cjs` build just works for consumers on a supported
Node.

## Changes

- Set the root `engines.node` to `>=22.12.0`.
- Add a yarn constraint (`enforceNodeEngineOnPackages` in
`yarn.config.cjs`) that applies the same range to every `packages/*`
workspace, applied with `yarn constraints --fix`. (CI's `yarn
constraints` keeps it consistent going forward.)
- Update the AGENTS.md setup note to match.

## Why per-package, not just the root

The root `package.json` is `private` and is **never published** — its
`engines` only constrains the monorepo dev environment; a consumer never
sees it. When someone runs `npm install @tldraw/editor`, npm reads
**that package's own** published `engines` and warns on a mismatch.
There's no "monorepo root" in what they install (just a single package
tarball), and npm has no notion of inheriting `engines` from a workspace
root.

| Where `engines` lives | Who it affects |
| --- | --- |
| root `package.json` (private) | tldraw developers building the
monorepo |
| each published package | consumers who install that package |

So to actually signal the requirement to consumers, the field has to be
on the packages they install. Each is independently installable (someone
might pull in just `@tldraw/store`), so each declares its own; the yarn
constraint keeps them in sync. Note this is a new pattern for the repo —
until now `engines` lived only at the root.

## Notes

- **`engines` is advisory** — npm only *warns* on an unmet engine, it
doesn't hard-fail the install. So this won't break anyone's build; it
signals the supported range.
- Alternative/complement: #9050 solves the same ESM-only-in-CJS problem
at the build level (inlining ESM-only deps into the CJS output). The
`@tldraw/mermaid` lazy-import that previously rode along here is now its
own PR: #9100.

### Change type

- [x] `improvement`

### Test plan

1. `yarn constraints` and `yarn check-packages` pass (engines applied
consistently to every `packages/*`).
2. Published packages now declare `engines.node: ">=22.12.0"`.

### Release notes

- Drop support for the EOL Node 20. The minimum supported Node is now
`>=22.12.0` — the version where `require()` of an ES module is supported
natively, which lets tldraw safely depend on modern ESM-only packages.

### Code changes

| Section        | LOC change |
| -------------- | ---------- |
| Documentation  | +1 / -1    |
| Config/tooling | +71 / -2   |
In order to keep the keyboard shortcuts dialog in sync with the
shortcuts the editor actually handles, this PR adds the shortcuts that
had a keybinding but were missing from the dialog. This follows up on
#7399 (copy as PNG), which added the shortcut but never listed it, and
covers the other gaps noted in #9087 (flatten, toggle locked, and more).

Added rows:

- Tools: insert embed (`cmd+i`), highlight tool (`shift+d`)
- Edit: copy as PNG (`cmd+shift+c`), print (`cmd+p`)
- Transform: frame selection (`cmd+alt+g`), flatten (`shift+f`), toggle
locked (`shift+l`), distribute horizontally (`alt+shift+h`), distribute
vertically (`alt+shift+v`)

A few shortcuts are intentionally left out and are now documented in the
new test's `NOT_IN_SHORTCUTS_DIALOG` list: cursor-targeted zoom/paste
variants and rotation (duplicates of rows already shown or covered by
the accessibility group), style-picking shortcuts, geo-tool re-select,
page navigation (no dialog labels), and cursor chat (only shown with
collaboration UI enabled).

A test now fails if a shortcut gains a keybinding without being added to
the dialog or to that intentional-omission list, so the dialog can't
silently drift again.

Closes #9087

### Change type

- [x] `improvement`

### Test plan

1. Open the editor and press the keyboard shortcuts shortcut
(`cmd+alt+/`).
2. Confirm the new rows appear: insert embed, highlight, copy as PNG,
print, frame selection, flatten, toggle locked, distribute
horizontally/vertically.

- [x] Unit tests

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +9 / -0    |
| Tests     | +59 / -0   |

### Release notes

- Add the missing keyboard shortcuts (copy as PNG, print, insert embed,
highlight tool, frame selection, flatten, toggle locked, distribute
horizontally/vertically) to the keyboard shortcuts dialog.
In order to fix sticky note attribution text rendering larger and
blurrier in PNG/SVG exports than on the live canvas, this PR renders the
attribution as HTML inside a `<foreignObject>` instead of as a native
SVG `<text>` element. Closes #8941.

The note body text was already exported through `RichTextSVG`, which
wraps HTML in a `<foreignObject>` so the rasterized output uses the
exact same layout and font metrics as the live DOM. The attribution was
the only element exported as a native SVG `<text>`, which the browser's
SVG text engine lays out and antialiases with different metrics — that
mismatch was the degradation. Rendering it through a `foreignObject`
with the existing `tl-note__attribution` class makes the export match
the DOM.

Because the `foreignObject` now applies the real `.tl-note__attribution`
CSS (`max-width: 60%; overflow: hidden; text-overflow: ellipsis`),
ellipsis truncation happens natively and identically to the canvas, so
the manual `truncateAttributionForSvg` text-measurement helper is no
longer needed and has been removed.

### Change type

- [x] `bugfix`

### Test plan

1. Open a sticky note and type some text so the attribution (first name)
appears in the bottom-right.
2. Copy the note as PNG (or export to PNG/SVG) and compare the pasted
result against the original.
3. Confirm the attribution text is crisp and the same size/weight as on
canvas, at both `scale = 1` and a resized note (`scale ≠ 1`).
4. Use a long attribution name and confirm it truncates with an ellipsis
the same way as on the canvas.

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

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +21 / -38  |

### Release notes

- Fix sticky note attribution text rendering larger and blurrier than
the canvas when a note is exported or copied as a PNG.
…OS input zoom (#9308)

In order to clean up a few rough edges on tldraw.com, this PR bundles
three small dotcom fixes:

- **Theme tokens** — standardize text colors on `--tl-color-text-0`
across the sidebar, top panel, menus, dialogs, buttons, and admin page.
Several rules referenced `--tl-color-text` and `--tl-color-text-1`,
which led to inconsistent text contrast.
- **Auth dialog** — narrow the auth dialog from 450px to 360px and drop
a redundant mobile `.authBody` override so the layout reads better.
- **iOS input zoom** — bump form fields to 16px on iOS only (gated
behind `@supports (-webkit-touch-callout: none)`) so Safari no longer
zooms in when a text field is focused.

### Change type

- [x] `bugfix`

### Test plan

1. On iOS Safari, focus the sidebar search input (and other text fields)
and confirm the page no longer zooms in.
2. Open the auth dialog and confirm it renders at the narrower width on
desktop and mobile.
3. Check the sidebar, top panel, menus, dialogs, and admin page for
consistent text color in light and dark themes.

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

### Release notes

- Fix iOS Safari zooming in when focusing text fields on tldraw.com.
- Standardize text colors and narrow the auth dialog.

### Code changes

| Section | LOC change |
| ------- | ---------- |
| Apps    | +76 / -69  |
In order to make pasting URLs feel responsive even when bookmark
metadata is slow to fetch, this PR creates the bookmark shape
immediately as a placeholder and hydrates it with metadata in the
background. The placeholder shows a loading spinner overlay (similar to
image loading) so the loading state is visible at the paste location,
and once metadata resolves the asset is patched in. If the fetch fails,
the bookmark gracefully falls back to a URL-only card.

<img width="2272" height="1396" alt="Kapture 2026-06-18 at 12 28 53"
src="https://github.com/user-attachments/assets/f0b9516c-59b9-4cec-a9a8-755c0be0693d"
/>

Closes #8653

### Change type

- [x] `feature`

### Test plan

1. Run `yarn dev`, open the examples app, and paste a URL onto the
canvas — a bookmark placeholder should appear immediately at the paste
location.
2. Throttle the network in DevTools and paste again — the placeholder
with its loading spinner should be visible while metadata loads, then
resolve into the full bookmark preview.
3. Paste a recognized embed URL (e.g. a YouTube link) — it should still
create an embed shape, not a bookmark placeholder.
4. Paste a URL that fails to unfurl — the bookmark should remain visible
as a URL-only card rather than disappearing.
5. Paste a URL, then immediately undo — the placeholder should disappear
cleanly without leaving an extra "metadata loaded" undo step.

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

### Release notes

- Pasted bookmarks now show an immediate loading placeholder at the
paste location, with a loading spinner, instead of waiting for metadata
before appearing.

### API changes

- BookmarkShapeUtil now uses `getGeometry` to explicitly define its
geometry.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pull pull Bot locked and limited conversation to collaborators Jun 18, 2026
@pull pull Bot added the ⤵️ pull label Jun 18, 2026
@pull
pull Bot merged commit e86c905 into code:main Jun 18, 2026
@pull
pull Bot had a problem deploying to deploy-staging June 18, 2026 15:13 Error
@pull
pull Bot had a problem deploying to deploy-staging June 18, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to bemo-canary June 18, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to bemo-canary June 18, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to vsce publish June 18, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to deploy-production June 18, 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.

6 participants