Conversation
…erflowingToolbar (#8690) In order to fix #8689, this PR reverts #8574, which removed the `attributes` and `characterData` options from the `MutationObserver` in `OverflowingToolbar`. With those options gone, switching directly between two geo variants (e.g. pressing `R` then `O`) only re-renders the affected `ToolbarItem` children, not `OverflowingToolbar` itself, so its `useLayoutEffect` does not fire and `lastActiveOverflowItem` stays pinned to the previous geo variant — leaving the wrong icon in the toolbar slot. Restoring the attribute/character-data observation makes those tool-selection updates trigger an overflow recompute again, so the toolbar slot swaps correctly when the active geo variant changes. This re-introduces the intermittent `ResizeObserver loop completed with undelivered notifications` warning from #8528. We'll revisit a better fix that doesn't drop attribute observation — likely driving the recompute from a reactive value at the parent level, or filtering mutations to only those that actually change the visible toolbar layout. ### Change type - [x] `bugfix` ### Test plan 1. Open the examples app or tldraw.com. 2. Press `R` to select the rectangle tool — the toolbar shows the rectangle icon, selected. 3. Press `O` to select the oval tool. 4. Verify the toolbar slot swaps to the ellipse icon, with the selected indicator still on it. 5. Verify toolbar overflow still works correctly when resizing the window. ### Release notes - Fix toolbar geo icon not swapping when switching directly between geo variants (e.g. rectangle to ellipse). ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +2 / -0 | Made with [Cursor](https://cursor.com)
In order to clear a tsserver warning in `apps/dotcom/client`, this PR
removes the `"plugins": [{ "name": "next" }]` entry from its
`tsconfig.json`. The dotcom client app builds with Vite (see
`vite.config.ts` and the `dev`/`build` scripts in `package.json`) and
has no `next` dependency, so the plugin reference is dead config.
### Change type
- [x] `other`
### Test plan
1. Open `apps/dotcom/client` in an editor with tsserver running and
confirm the "next" plugin warning is gone.
2. Run `yarn typecheck` from the repo root and confirm it still passes.
### Code changes
| Section | LOC change |
| -------------- | ---------- |
| Config/tooling | +0 / -1 |
…ttributes (#8701) In order to fix the `ResizeObserver loop completed with undelivered notifications` warnings reported in #8528 without re-introducing the toolbar geo-icon regression in #8689, this PR replaces the broad `attributes: true` / `characterData: true` options on the `OverflowingToolbar` `MutationObserver` with `attributeFilter: ['aria-pressed', 'data-value']` — the only attributes `onDomUpdate` actually reads. Closes #8528 Background: - #8574 fixed #8528 by dropping `attributes` and `characterData` entirely. That broke #8689 because tool selection inside individual `ToolbarItem` children no longer notified the parent toolbar (only the affected children re-render, not `OverflowingToolbar` itself, so its `useLayoutEffect` doesn't fire). - #8690 reverted #8574, restoring #8689 but reopening #8528. - This PR replaces both with a targeted filter, so attribute observation still happens but only for the attributes that actually matter. Why the loop happened in the first place: 1. **Hover-driven attribute churn**: tooltip/popover wrappers flip `data-state`, `aria-describedby`, `aria-expanded` on toolbar descendants on hover/focus. With unfiltered observation, every flip re-ran `onDomUpdate`. 2. **Self-write feedback**: `onDomUpdate` writes `data-toolbar-visible` on items. Unfiltered observation re-fired the observer on those writes, creating a read→write→read cycle that the browser's ResizeObserver guards against. Why the geo-variant regression happened: - Pressing `R` then `O` only re-renders the rectangle and oval `ToolbarItem` children whose `useIsToolSelected` value changed — not `OverflowingToolbar` itself. Without attribute observation, the toolbar's `lastActiveOverflowItem` stayed pinned to the previous variant and the wrong icon kept showing in the visible slot. How `attributeFilter: ['aria-pressed', 'data-value']` resolves both: | Trigger | Attribute changed | In filter? | Observer fires? | | --- | --- | --- | --- | | Hover button | `data-state`, `aria-describedby` | no | no | | Open popover | `aria-expanded`, `data-state` | no | no | | Our own visibility write | `data-toolbar-visible` | no | no | | Active tool changes (incl. R then O) | `aria-pressed` | yes | yes | | Tool slot identity changes | `data-value` | yes | yes | | Add / remove a toolbar item | (childList) | yes | yes | | Window / container resize | (ResizeObserver) | n/a | yes | Both feedback paths into the loop are closed (hover-driven mutations and self-writes are excluded), and the legitimate recompute path needed by #8689 is preserved (`aria-pressed` flips still fire the observer). `characterData` is dropped: toolbar buttons are icon-only, and any genuine text-driven layout change (i18n locale switch) re-renders the parent and runs the unconditional `useLayoutEffect` already. ### Change type - [x] `bugfix` ### Test plan 1. Open the examples app or tldraw.com with the browser console open. 2. Move the pointer rapidly across the bottom toolbar buttons. Verify no `ResizeObserver loop completed with undelivered notifications` warnings appear. 3. Press `R` to select the rectangle tool, then `O` to select the oval tool. Verify the toolbar slot swaps to the ellipse icon with the selected indicator on it. 4. Resize the window so toolbar items overflow into the popover, then back. Verify items move in and out of the overflow correctly. 5. Open the overflow popover, click a tool. Verify it gets pinned into the visible toolbar slot. ### Release notes - Fix intermittent `ResizeObserver loop` browser warnings when hovering over toolbar buttons, while preserving correct icon swapping when switching directly between geo variants. ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +8 / -1 | Made with [Cursor](https://cursor.com)
#8705) In order to make the geo extensibility API self-consistent before its first release, this PR renames the `customGeoStyles` option on `GeoShapeUtil.configure()` back to `customGeoTypes`. Follow-up to #8543. The original PR introduced `customGeoTypes`, then it was renamed to `customGeoStyles` during review feedback. The rename was applied to the option name, but several adjacent surfaces were missed: - The exported `GeoTypeDefinition` interface (still uses "type") - The internal `getCustomGeoType` helper and `customType` locals - The `apps/examples/src/examples/shapes/tools/custom-geo-types/` example folder, README title, and `CustomGeoTypesExample` component - The release notes in `apps/docs/content/releases/next.mdx`, which still document `customGeoTypes` (so the example as published would not actually compile) Re-aligning everything on "types" is the smaller change and matches the conceptual model — `'rectangle' | 'ellipse' | 'rounded-rect'` are kinds of geo, not styles. The "style" naming would have collided with the existing `GeoShapeGeoStyle` `StyleProp` and the unrelated style panel terminology. This API has not shipped yet (it lives in `next.mdx`), so no released consumers are affected. ### Change type - [x] `api` ### Test plan 1. Open the `custom geo types` example. 2. Verify the rounded rectangle and cross shapes render, drag-create from the toolbar, and appear in the geo style panel picker. - [x] Unit tests (existing geo + style tests still pass) ### API changes - Renamed `GeoShapeOptions.customGeoStyles` to `GeoShapeOptions.customGeoTypes`. The option keys this map populates on `GeoShapeGeoStyle` are unaffected. ### Code changes | Section | LOC change | | --------------- | ---------- | | Core code | +27 / -27 | | Automated files | +1 / -1 | | Documentation | +11 / -11 | Made with [Cursor](https://cursor.com)
In order to keep the minimap from getting stuck in a dragging state, this PR ends the active drag when a right-click (contextmenu), `pointerup`, or `pointercancel` happens during a minimap drag. Previously, right-clicking the minimap mid-drag did not dispatch a `pointerup`, so pointer capture was never released and `rPointing` stayed true — subsequent pointer moves continued to recenter the camera until the user clicked again. The pointer being released is now tracked by id, so we release capture on the same pointer that started the drag instead of synthesising a release from the next event. Closes #8693 ### Change type - [x] `bugfix` ### Test plan 1. Run `yarn dev` and open an example with content so the minimap is interactive. 2. Press and hold the left mouse button on the minimap to start dragging the viewport. 3. While still holding, right-click on the minimap and dismiss the context menu. 4. Move the mouse over the canvas — the camera should not continue to follow the pointer. 5. Repeat with a `pointercancel` (e.g. touch interruption) and confirm the drag ends. - [ ] Unit tests - [ ] End to end tests ### Release notes - Fix minimap getting stuck in a dragging state when right-clicked (or when the pointer is cancelled) during a drag. ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +23 / -7 |
…8702) In order to promote the tldraw SDK to logged-out visitors of tldraw.com, this PR adds a dismissible "Build with the tldraw SDK" overlay link in the bottom-right of the editor — the same area used by the watermark. The overlay sits above the watermark layer (`z-index: calc(var(--tl-layer-watermark) + 1)`) so the watermark is not modified or hidden, and reappears underneath if the overlay is dismissed. Closes #8699. Behavior: - Renders only when Clerk's `useAuth().isSignedIn === false`, so signed-in users (and the still-loading state) never see it. - Dismissal is persisted in `localStorage` under `tldraw-dotcom:anon-dotdev-link:dismissed`, so it stays dismissed across reloads. - Hidden under 700px to match the watermark's mobile breakpoint. - Tracks the watermark's `data-debug` offset and RTL direction so it stays aligned in those modes. The component is rendered inside `<Tldraw>` in the local editor and the multiplayer file editor, so it shows on both the signed-out local editor and the signed-out file viewer. ### Change type - [x] `feature` ### Test plan 1. Sign out and visit `localhost:3000` — overlay appears bottom-right. 2. Click the link — opens `tldraw.dev` in a new tab with the `anon-overlay-link` UTM campaign. 3. Click the dismiss (X) button — overlay disappears. 4. Reload — overlay stays dismissed. 5. `localStorage.removeItem('tldraw-dotcom:anon-dotdev-link:dismissed')` and reload — overlay reappears. 6. Sign in — overlay no longer renders. 7. Visit a shared file URL while signed out — overlay renders there too. 8. Resize below ~700px — overlay hides on mobile. 9. Toggle dark mode — colors track the tldraw theme tokens. - [ ] Unit tests - [ ] End to end tests ### Release notes - Add a dismissible link to tldraw.dev in the bottom-right of the editor for signed-out visitors on tldraw.com.
In order to keep the style panel in sync when the app switches between light and dark mode, this PR subscribes the style panel color and font item rendering to the editor's current theme and color mode. Closes #8680. ### Change type - [x] `bugfix` ### Test plan 1. Select black in the style panel. 2. Switch from light mode to dark mode and back. 3. Confirm the black color swatch changes to white in dark mode and back to black in light mode without selecting another color. - [x] Unit tests - [ ] End to end tests ### Release notes - Fix style panel color swatches updating when switching between light and dark mode. ### API changes - Added `'menu.color-theme'` to the `TLUiTranslationKey` union. ### Code changes | Section | LOC change | | --------------- | ---------- | | Core code | +31 / -11 | | Tests | +46 / -0 | | Automated files | +1 / -1 | | Apps | +3 / -1 |
Bumps production Zero view-syncer count from 5 to 7 machines. Production VS machines are at 4-CPU saturation as we approach peak traffic. Symptoms: pipeline-resets jumped from 0 to ~0.033/s starting ~12:45 UTC, CPU climbing 50% → 65%+, slow SQLite queries (100-450 ms) on the small `_zero.changeLog2` table — slowness is contention-driven, not table size (only 16,646 rows). Memory is fine (~2 GB used of 8 GB). Routing is sticky-by-cookie (`fly_machine_id`), so adding machines spreads independent client groups across more capacity. Follows #8666 which went 4 → 5. ### Change type - [x] `other` ### Test plan - After merge: `flyctl status -a production-zero-vs` shows 7 started machines. - Confirm load avg drops back below 4.0 per machine and pipeline-resets metric returns toward 0. ### Release notes - Internal: scale Zero view-syncer to 7 machines.
…lls (#8710) In order to remove the white square visible at the base of the collaborator cursor, this PR splits each cursor layer (shadow, white outline, colored fill) into two separate `Path2D` objects — one for the arrowhead, one for the tail — and fills them with two separate `ctx.fill()` calls per layer. Closes #8692. The previous implementation packed both subpaths into one `Path2D` per layer and filled them in a single call. With the default `nonzero` winding rule, the canvas renderer evaluates both subpaths together as one compound shape; along the seam where the head and tail overlap, this produced sub-pixel rasterisation artefacts that surfaced as a visible white sliver/square at the base of the tail in some DPR + zoom combinations. The original SVG-DOM implementation did not have this problem because each `<path>` was rendered with its own fill operation, with no compound-shape rasterisation across the shared boundary. The split mirrors the original SVG's two-`<path>`-per-`<g>` layout and matches its rendering behaviour exactly. The white tail's path coordinates were also corrected from a 4×12 parallelogram to a 4×10 one — a 1-unit perpendicular outline of the 2×8 colored fill tail — so the white outline no longer extends past the colored fill. ### Change type - [x] `bugfix` ### Test plan 1. Open a multiplayer example (e.g. `multiplayer-demo`) with another collaborator. 2. Observe the remote collaborator's cursor at default zoom on a HiDPI display. 3. Confirm the cursor renders as a single colored arrow with a thin white outline; no white rectangle visible at the base of the tail. 4. Zoom in/out and pan; confirm the cursor stays clean at all zoom levels. - [x] Unit tests ### Release notes - Fix a white square artefact that appeared at the base of remote collaborator cursors.
In order to fix the bug where notes display a solid line at the bottom edge in non-default dark mode themes (Ocean, Sunset, and any developer-registered theme via #8410), this PR removes the `if (colorMode === 'dark') hideShadows = true` shortcut in `NoteShapeUtil.tsx`. With the shortcut gone, dark-mode notes render the existing 3-layer box-shadow at normal zoom — the same path light mode already uses. On the default-dark canvas the hardcoded `rgba(15, 23, 31, ...)` shadow is essentially invisible, so default-dark looks unchanged; on lighter dark-theme backgrounds the shadow becomes visible and reads correctly, with no more solid line. Closes #8688. ### Why this is safe perf-wise The dark-mode shortcut was a "free" perf bonus (skip an invisible shadow), not load-bearing. The actual perf optimization is `useEfficientZoomThreshold(0.25 / scale)`, which is mode-agnostic and still active. After this change: - Light mode, normal zoom: 3-layer box-shadow per note (unchanged). - Light mode, low zoom: borderBottom (perf path, unchanged). - Dark mode, normal zoom: 3-layer box-shadow per note — now matches light mode. (Was: borderBottom.) - Dark mode, low zoom: borderBottom (perf path, unchanged). Dark-mode at normal zoom now hits the same compositor path as light-mode at normal zoom, with the same shadow string. If light mode perf is acceptable, dark mode will be too — they're equalized, not regressed. ### Change type - [x] `bugfix` ### Test plan 1. Open the multiple-themes example (`apps/examples/src/examples/ui/multiple-themes/`), switch to dark mode, switch through Default / Ocean / Sunset, drop a note in each. 2. Confirm no conspicuous solid line under notes on Ocean / Sunset and unchanged appearance on default-dark. 3. Verify low-zoom borderBottom still kicks in on dark mode (zoom out past the threshold). - [x] Unit tests ### Release notes - Fix note shapes rendering a solid line at the bottom edge in non-default dark mode themes. ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +2 / -3 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 : )