Merge branch 'main' into ignore-locked-hovered-shape

pull/3575/head
Steve Ruiz 2024-04-27 13:29:56 +01:00
commit d51899aeda
209 zmienionych plików z 3970 dodań i 212432 usunięć

Wyświetl plik

@ -46,12 +46,12 @@ module.exports = {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'import/no-extraneous-dependencies': 'error',
'import/no-internal-modules': ['error', { forbid: ['@tldraw/*/**', 'tldraw/**'] }],
'@typescript-eslint/consistent-type-exports': [
'error',
{ fixMixedExportsWithInlineTypeSpecifier: true },
],
'local/no-export-star': 'error',
'local/no-internal-imports': 'error',
'no-only-tests/no-only-tests': 'error',
'no-restricted-syntax': [
'error',
@ -99,7 +99,6 @@ module.exports = {
{
files: ['apps/examples/**/*'],
rules: {
'import/no-internal-modules': 'off',
'no-restricted-syntax': 'off',
},
},

3
.gitignore vendored
Wyświetl plik

@ -74,6 +74,7 @@ apps/examples/www/translations
!.yarn/versions
packages/*/api/temp
packages/*/api/api.json
packages/*/api/internal.d.ts
packages/*/api/public.d.ts
@ -87,6 +88,8 @@ apps/examples/e2e/test-results
apps/examples/playwright-report
apps/docs/content/gen
apps/docs/content/releases
apps/docs/api
.dev.vars
.env.local

Wyświetl plik

@ -4,5 +4,4 @@
yarn install --immutable
npx lazy run build-api
git add packages/*/api-report.md
git add packages/*/api/api.json
npx lint-staged

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -17,6 +17,674 @@ order: 3
{/* START AUTO-GENERATED CHANGELOG */}
### [v2.1.0](/releases/v2.1.0)
#### Make note handles show only one when zoomed out ([#3562](https://github.com/tldraw/tldraw/pull/3562))
- Show only the bottom handle on notes when zoomed between .25 and .5
#### Perf: minor drawing speedup ([#3464](https://github.com/tldraw/tldraw/pull/3464))
- Improve performance of draw shapes.
#### Prevent default on native clipboard events ([#3536](https://github.com/tldraw/tldraw/pull/3536))
- Fix copy sound on clipboard events.
#### WebGL Minimap ([#3510](https://github.com/tldraw/tldraw/pull/3510))
- Add a brief release note for your PR here.
#### arrows: fix bound arrow labels going over text shape ([#3512](https://github.com/tldraw/tldraw/pull/3512))
- Arrows: fix label positioning when bound.
#### arrows: still use Dist instead of Dist2 ([#3511](https://github.com/tldraw/tldraw/pull/3511))
- Fix arrow label positioning
#### Fix culling. ([#3504](https://github.com/tldraw/tldraw/pull/3504))
- Fix culling.
#### "Soft preload" icons ([#3507](https://github.com/tldraw/tldraw/pull/3507))
- Improve icon preloading
#### Color tweaks (light and dark mode) ([#3486](https://github.com/tldraw/tldraw/pull/3486))
- Adjusts colors
#### Add slides example ([#3467](https://github.com/tldraw/tldraw/pull/3467))
- Docs: Added a slideshow example
#### Only show cursor chat button in select mode ([#3485](https://github.com/tldraw/tldraw/pull/3485))
- Fix cursor chat button appearing when not in select tool.
#### Fix alt-duplicating shapes sometimes not working ([#3488](https://github.com/tldraw/tldraw/pull/3488))
- Add a brief release note for your PR here.
#### [perf] faster signia capture (again) ([#3487](https://github.com/tldraw/tldraw/pull/3487))
- Add a brief release note for your PR here.
#### [perf] faster signia capture ([#3471](https://github.com/tldraw/tldraw/pull/3471))
- Slight performance improvement to reactivity bookkeeping.
#### New migrations again ([#3220](https://github.com/tldraw/tldraw/pull/3220))
#### BREAKING CHANGES
- The `Migrations` type is now called `LegacyMigrations`.
- The serialized schema format (e.g. returned by `StoreSchema.serialize()` and `Store.getSnapshot()`) has changed. You don't need to do anything about it unless you were reading data directly from the schema for some reason. In which case it'd be best to avoid that in the future! We have no plans to change the schema format again (this time was traumatic enough) but you never know.
- `compareRecordVersions` and the `RecordVersion` type have both disappeared. There is no replacement. These were public by mistake anyway, so hopefully nobody had been using it.
- `compareSchemas` is gone. Comparing the schemas directly is no longer really possible since we introduced some fuzziness. The best thing to do now to check compatibility is to call `schema.getMigraitonsSince(prevSchema)` and it will return an error if the schemas are not compatible, an empty array if there are no migrations to apply since the prev schema, and a nonempty array otherwise.
Generally speaking, the best way to check schema compatibility now is to call `store.schema.getMigrationsSince(persistedSchema)`. This will throw an error if there is no upgrade path from the `persistedSchema` to the current version.
- `defineMigrations` has been deprecated and will be removed in a future release. For upgrade instructions see https://tldraw.dev/docs/persistence#Updating-legacy-shape-migrations-defineMigrations
- `migrate` has been removed. Nobody should have been using this but if you were you'll need to find an alternative. For migrating tldraw data, you should stick to using `schema.migrateStoreSnapshot` and, if you are building a nuanced sync engine that supports some amount of backwards compatibility, also feel free to use `schema.migratePersistedRecord`.
- the `Migration` type has changed. If you need the old one for some reason it has been renamed to `LegacyMigration`. It will be removed in a future release.
- the `Migrations` type has been renamed to `LegacyMigrations` and will be removed in a future release.
- the `SerializedSchema` type has been augmented. If you need the old version specifically you can use `SerializedSchemaV1`
#### Stickies: release candidate ([#3249](https://github.com/tldraw/tldraw/pull/3249))
- Improves sticky notes (see list)
#### Cancel pointer velocity while pinching ([#3462](https://github.com/tldraw/tldraw/pull/3462))
- Fixed a bug that could occur while pinching with the hand tool selected.
#### conditionally use star-history dark theme ([#3461](https://github.com/tldraw/tldraw/pull/3461))
updates the star-history image in the README to conditionally show a dark theme image based on the user's `prefers-color-scheme`
#### Allow users to edit the document title by double clicking it even when editing a shape. ([#3459](https://github.com/tldraw/tldraw/pull/3459))
- Allow users to editing document name by double clicking even when previously editing text.
#### Don't show edit link for locked shapes. ([#3457](https://github.com/tldraw/tldraw/pull/3457))
- Hide edit link context menu option for locked shapes.
#### Faster selection / erasing ([#3454](https://github.com/tldraw/tldraw/pull/3454))
- Improve performance of minimum distance checks.
#### Make minimap display sharp rectangles. ([#3434](https://github.com/tldraw/tldraw/pull/3434))
- Improve
#### Update font import URL in quick-start.mdx ([#3430](https://github.com/tldraw/tldraw/pull/3430))
- Fixes font import link in tldraw.dev quickstart guide
#### Perf: Improve text outline performance ([#3429](https://github.com/tldraw/tldraw/pull/3429))
- Improves performance of text shapes on iOS / Safari.
#### Perf: throttle `updateHoveredId` ([#3419](https://github.com/tldraw/tldraw/pull/3419))
- Improves canvas performance by throttling the update to the editor's hovered id.
#### Perf: block hit tests while moving camera ([#3418](https://github.com/tldraw/tldraw/pull/3418))
- Improves performance of canvas while the camera is moving.
#### Perf: (slightly) faster min dist checks ([#3401](https://github.com/tldraw/tldraw/pull/3401))
- Performance: small improvements to hit testing.
#### Examples: update kbd shortcuts, add actions overrides example ([#3330](https://github.com/tldraw/tldraw/pull/3330))
- Add action overrides example, update keyboard shortcuts example
#### Add long press event ([#3275](https://github.com/tldraw/tldraw/pull/3275))
- Add support for long pressing on desktop.
#### Tool with child states ([#3074](https://github.com/tldraw/tldraw/pull/3074))
- Add an example of a tool with child states
#### Fix text resizing bug ([#3327](https://github.com/tldraw/tldraw/pull/3327))
- Fixes an issue with text shapes overflowing their bounds when resized.
#### Input buffering ([#3223](https://github.com/tldraw/tldraw/pull/3223))
- Add a brief release note for your PR here.
#### Add white ([#3321](https://github.com/tldraw/tldraw/pull/3321))
- Adds secret white color.
#### Don't trigger pointer move on zoom ([#3305](https://github.com/tldraw/tldraw/pull/3305))
- Improve performance of zooming.
#### Decrease the number of rendered dom nodes for geo shape and arrows ([#3283](https://github.com/tldraw/tldraw/pull/3283))
- Reduce the number of rendered dom nodes for geo shapes and arrows without text.
#### Improve performance of culling ([#3272](https://github.com/tldraw/tldraw/pull/3272))
- Improve performance of the canvas when many shapes are present.
#### styling: make dotcom and examples site have consistent font styling ([#3271](https://github.com/tldraw/tldraw/pull/3271))
- Add a brief release note for your PR here.
#### ui: make toasts look more toasty ([#2988](https://github.com/tldraw/tldraw/pull/2988))
- UI: Add severity to toasts.
#### textfields [1 of 3]: add text into speech bubble; also add rich text example ([#3050](https://github.com/tldraw/tldraw/pull/3050))
- Refactor textfields be composable/swappable.
#### Update romanian translations ([#3269](https://github.com/tldraw/tldraw/pull/3269))
- Update Romanian translation.
#### Allow hiding debug panel. ([#3261](https://github.com/tldraw/tldraw/pull/3261))
- Allow users to fully override the `DebugPanel`.
#### Add inline behaviour example ([#3113](https://github.com/tldraw/tldraw/pull/3113))
- Docs: Added an example for inline behaviour.
#### toolbar: fix missing title attributes ([#3244](https://github.com/tldraw/tldraw/pull/3244))
- Fix title's being missing on toolbar items.
#### Don't double squash ([#3182](https://github.com/tldraw/tldraw/pull/3182))
- Minor improvement when modifying multiple shapes at once.
#### Fix lag while panning + translating at the same time ([#3186](https://github.com/tldraw/tldraw/pull/3186))
- Add a brief release note for your PR here.
#### fix docs build ([#3201](https://github.com/tldraw/tldraw/pull/3201))
- Add a brief release note for your PR here.
#### Update the document title to include the document name. ([#3197](https://github.com/tldraw/tldraw/pull/3197))
- Use the document name in the `document.title`.
#### Remove access token logic. ([#3187](https://github.com/tldraw/tldraw/pull/3187))
- Remove some leftover logic from pro days.
#### [fix] Batch tick events ([#3181](https://github.com/tldraw/tldraw/pull/3181))
- Fix a performance issue effecting resizing multiple shapes.
#### [tinyish] Simplify / skip some work in Shape ([#3176](https://github.com/tldraw/tldraw/pull/3176))
- SDK: minor improvements to the Shape component
#### [tiny] Slightly more efficient selection rotated page bounds / page bounds ([#3178](https://github.com/tldraw/tldraw/pull/3178))
- SDK, slightly more performant selection bounds calculations.
#### [sync] allow connections from v4 clients ([#3173](https://github.com/tldraw/tldraw/pull/3173))
- Add a brief release note for your PR here.
#### [fix] Handles extra renders ([#3172](https://github.com/tldraw/tldraw/pull/3172))
- SDK: Fixed a minor rendering issue related to handles.
#### [fix] Cleanup text measures ([#3169](https://github.com/tldraw/tldraw/pull/3169))
- Fixed a bug that could cause multiple text measurement divs in development mode.
#### [perf] Reinstate render throttling ([#3160](https://github.com/tldraw/tldraw/pull/3160))
- Add a brief release note for your PR here.
#### Fix release eliding ([#3156](https://github.com/tldraw/tldraw/pull/3156))
- Add a brief release note for your PR here.
#### Updated exploded example link from installation page. ([#3138](https://github.com/tldraw/tldraw/pull/3138))
- Add a brief release note for your PR here.
Installation docs has a link to example for exploded which points to github 404. I have updated the working link.
#### Make the custom menu examples a bit clearer ([#3106](https://github.com/tldraw/tldraw/pull/3106))
- Add a brief release note for your PR here.
#### Menu updates / fix flip / add export / remove Shape menu ([#3115](https://github.com/tldraw/tldraw/pull/3115))
- Revert some changes in the menu.
#### Performance improvements ([#2977](https://github.com/tldraw/tldraw/pull/2977))
- Improves the performance of rendering.
#### [fix] Rotated crop handle ([#3093](https://github.com/tldraw/tldraw/pull/3093))
- Fixed a bug that could cause rotated cropping images to have incorrectly rotated handles.
#### Fix typo in useValue comment ([#3088](https://github.com/tldraw/tldraw/pull/3088))
- Fix typo in useValue comment.
#### Shape with Migrations ([#3078](https://github.com/tldraw/tldraw/pull/3078))
- Adds a shape with migrations example
#### Fix viewport params for pages. ([#3079](https://github.com/tldraw/tldraw/pull/3079))
- Fixes an issue with url params in the share links. The viewport params only worked on the first page in the document.
#### Fix typo ([#3069](https://github.com/tldraw/tldraw/pull/3069))
N/A
#### Add custom tool examples ([#3064](https://github.com/tldraw/tldraw/pull/3064))
- Adds a simple custom tool example
#### Fix validation errors for `duplicateProps` ([#3065](https://github.com/tldraw/tldraw/pull/3065))
- Add a brief release note for your PR here.
#### Shorten url state ([#3041](https://github.com/tldraw/tldraw/pull/3041))
- Shortens url parameters for dot com.
#### Fix an issue where the video size was not drawn correctly ([#3047](https://github.com/tldraw/tldraw/pull/3047))
- Fix an issue where the video size was not drawn correctly.
#### [fix] Input tags ([#3038](https://github.com/tldraw/tldraw/pull/3038))
- Fixed autocomplete, autocapitalize, and autocorrect tags on text inputs.
#### Lokalise: Translations update ([#3049](https://github.com/tldraw/tldraw/pull/3049))
- Updated Hungarian translations.
#### [terrible] Firefox: Allow scrolling on keyboard shortcuts dialog ([#2974](https://github.com/tldraw/tldraw/pull/2974))
- Add a brief release note for your PR here.
#### Fix cursor chat bubble position. ([#3042](https://github.com/tldraw/tldraw/pull/3042))
- Fixed a bug where cursor chat bubble position could be wrong when a sidebar was open.
#### Fix broken link for shape example ([#3046](https://github.com/tldraw/tldraw/pull/3046))
- Fix a link that was pointing to a 404 on GitHub
#### Protect local storage calls ([#3043](https://github.com/tldraw/tldraw/pull/3043))
- Fixes a bug that could cause crashes in React Native webviews.
#### Custom shape examples ([#2994](https://github.com/tldraw/tldraw/pull/2994))
- adds a simple custom shape example
- adds an interactive shape example
- updates editable shape example
#### Expose `getStyleForNextShape` ([#3039](https://github.com/tldraw/tldraw/pull/3039))
- Expose the API for `Editor.getStyleForNextShape`, previously marked as internal.
#### [fix] Missing element crash (rare) on video shapes. ([#3037](https://github.com/tldraw/tldraw/pull/3037))
- Fixed a rare crash with video shapes.
#### Example of using tldraw styles ([#3017](https://github.com/tldraw/tldraw/pull/3017))
- shape with tldraw styles example
#### Show a broken image for files without assets ([#2990](https://github.com/tldraw/tldraw/pull/2990))
- Better handling of broken images / videos.
#### Selection UI example (plus fixes to pageToScreen) ([#3015](https://github.com/tldraw/tldraw/pull/3015))
- Adds selection UI example.
- Adds `Editor.getSelectionRotatedScreenBounds` method
- Fixes a bug with `pageToScreen`.
#### [bugfix] Avoid randomness at init time to allow running on cloudflare. ([#3016](https://github.com/tldraw/tldraw/pull/3016))
- Prevent using randomness API at init time, to allow importing the tldraw package in a cloudflare worker.
---
#### 💥 Breaking Change
- `@tldraw/editor`, `tldraw`, `@tldraw/validate`
- React-powered SVG exports [#3117](https://github.com/tldraw/tldraw/pull/3117) ([@SomeHats](https://github.com/SomeHats) [@huppy-bot[bot]](https://github.com/huppy-bot[bot]))
- `@tldraw/editor`, `tldraw`
- Component-based toolbar customisation API [#3067](https://github.com/tldraw/tldraw/pull/3067) ([@SomeHats](https://github.com/SomeHats) [@steveruizok](https://github.com/steveruizok))
- `tldraw`
- Menu updates / fix flip / add export / remove Shape menu [#3115](https://github.com/tldraw/tldraw/pull/3115) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/state`, `@tldraw/store`, `tldraw`, `@tldraw/utils`
- Performance improvements [#2977](https://github.com/tldraw/tldraw/pull/2977) ([@MitjaBezensek](https://github.com/MitjaBezensek) [@steveruizok](https://github.com/steveruizok))
#### 🚀 Enhancement
- squish sync data events before sending them out [#3118](https://github.com/tldraw/tldraw/pull/3118) ([@si14](https://github.com/si14))
- `@tldraw/editor`, `tldraw`
- textfields [1 of 3]: add text into speech bubble; also add rich text example [#3050](https://github.com/tldraw/tldraw/pull/3050) ([@mimecuvalo](https://github.com/mimecuvalo))
- `@tldraw/editor`
- Selection UI example (plus fixes to pageToScreen) [#3015](https://github.com/tldraw/tldraw/pull/3015) ([@steveruizok](https://github.com/steveruizok))
#### 🐛 Bug Fix
- Revert "squish sync data events before sending them out" [#3331](https://github.com/tldraw/tldraw/pull/3331) ([@ds300](https://github.com/ds300))
- docs: fix up github link [#3108](https://github.com/tldraw/tldraw/pull/3108) ([@mimecuvalo](https://github.com/mimecuvalo))
- Bump the npm_and_yarn group across 3 directories with 3 updates [#3087](https://github.com/tldraw/tldraw/pull/3087) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@github-actions[bot]](https://github.com/github-actions[bot]) [@MitjaBezensek](https://github.com/MitjaBezensek))
- simplify fnmatch pattern ([@ds300](https://github.com/ds300))
- Bump the npm_and_yarn group group with 7 updates [#2982](https://github.com/tldraw/tldraw/pull/2982) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@MitjaBezensek](https://github.com/MitjaBezensek))
- `tldraw`
- quick fixes [#3128](https://github.com/tldraw/tldraw/pull/3128) ([@steveruizok](https://github.com/steveruizok))
#### 📚 SDK Changes
- Update useFileSystem.tsx [#3371](https://github.com/tldraw/tldraw/pull/3371) ([@steveruizok](https://github.com/steveruizok))
- only buffer pointer events [#3337](https://github.com/tldraw/tldraw/pull/3337) ([@steveruizok](https://github.com/steveruizok))
- `tldraw`
- Fix collaborator size with zoom [#3563](https://github.com/tldraw/tldraw/pull/3563) ([@steveruizok](https://github.com/steveruizok))
- Make note handles show only one when zoomed out [#3562](https://github.com/tldraw/tldraw/pull/3562) ([@steveruizok](https://github.com/steveruizok))
- Fix transparent colors in the minimap [#3561](https://github.com/tldraw/tldraw/pull/3561) ([@steveruizok](https://github.com/steveruizok))
- Expose `usePreloadAssets` [#3545](https://github.com/tldraw/tldraw/pull/3545) ([@SomeHats](https://github.com/SomeHats))
- Prevent default on native clipboard events [#3536](https://github.com/tldraw/tldraw/pull/3536) ([@steveruizok](https://github.com/steveruizok))
- Improve back to content [#3532](https://github.com/tldraw/tldraw/pull/3532) ([@steveruizok](https://github.com/steveruizok))
- arrows: fix bound arrow labels going over text shape [#3512](https://github.com/tldraw/tldraw/pull/3512) ([@mimecuvalo](https://github.com/mimecuvalo))
- textfields: fix Safari cursor rendering bug, take 2 [#3513](https://github.com/tldraw/tldraw/pull/3513) ([@mimecuvalo](https://github.com/mimecuvalo))
- geo: fix double unique id on DOM [#3514](https://github.com/tldraw/tldraw/pull/3514) ([@mimecuvalo](https://github.com/mimecuvalo))
- arrows: still use Dist instead of Dist2 [#3511](https://github.com/tldraw/tldraw/pull/3511) ([@mimecuvalo](https://github.com/mimecuvalo))
- textfields: nix disableTab option; make TextShapes have custom Tab behavior as intended [#3506](https://github.com/tldraw/tldraw/pull/3506) ([@mimecuvalo](https://github.com/mimecuvalo))
- "Soft preload" icons [#3507](https://github.com/tldraw/tldraw/pull/3507) ([@steveruizok](https://github.com/steveruizok))
- textfields: wait a tick before selecting all to fix iOS [#3501](https://github.com/tldraw/tldraw/pull/3501) ([@mimecuvalo](https://github.com/mimecuvalo))
- textfields: fix dragging selected shape behind another [#3498](https://github.com/tldraw/tldraw/pull/3498) ([@mimecuvalo](https://github.com/mimecuvalo))
- stickies: a bit of fuzziness when calculating certain text [#3493](https://github.com/tldraw/tldraw/pull/3493) ([@mimecuvalo](https://github.com/mimecuvalo))
- Fix alt-duplicating shapes sometimes not working [#3488](https://github.com/tldraw/tldraw/pull/3488) ([@TodePond](https://github.com/TodePond))
- stickies: dont remove selection ranges when edit->edit [#3484](https://github.com/tldraw/tldraw/pull/3484) ([@mimecuvalo](https://github.com/mimecuvalo))
- stickies: hide clone handles on mobile [#3478](https://github.com/tldraw/tldraw/pull/3478) ([@mimecuvalo](https://github.com/mimecuvalo))
- Don't show edit link for locked shapes. [#3457](https://github.com/tldraw/tldraw/pull/3457) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Performance measurement tool (for unit tests) [#3447](https://github.com/tldraw/tldraw/pull/3447) ([@steveruizok](https://github.com/steveruizok))
- Remove minimap throttling [#3438](https://github.com/tldraw/tldraw/pull/3438) ([@steveruizok](https://github.com/steveruizok))
- Make minimap display sharp rectangles. [#3434](https://github.com/tldraw/tldraw/pull/3434) ([@steveruizok](https://github.com/steveruizok))
- Perf: throttle `updateHoveredId` [#3419](https://github.com/tldraw/tldraw/pull/3419) ([@steveruizok](https://github.com/steveruizok))
- Revert "Fix text resizing bug (#3327)" [#3332](https://github.com/tldraw/tldraw/pull/3332) ([@mimecuvalo](https://github.com/mimecuvalo))
- Fix text resizing bug [#3327](https://github.com/tldraw/tldraw/pull/3327) ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))
- Fix count shapes and nodes [#3318](https://github.com/tldraw/tldraw/pull/3318) ([@steveruizok](https://github.com/steveruizok))
- Decrease the number of rendered dom nodes for geo shape and arrows [#3283](https://github.com/tldraw/tldraw/pull/3283) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- styling: make dotcom and examples site have consistent font styling [#3271](https://github.com/tldraw/tldraw/pull/3271) ([@mimecuvalo](https://github.com/mimecuvalo) [@steveruizok](https://github.com/steveruizok))
- Allow hiding debug panel. [#3261](https://github.com/tldraw/tldraw/pull/3261) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- toolbar: fix missing title attributes [#3244](https://github.com/tldraw/tldraw/pull/3244) ([@mimecuvalo](https://github.com/mimecuvalo) [@steveruizok](https://github.com/steveruizok))
- Fix jpg export and tests [#3198](https://github.com/tldraw/tldraw/pull/3198) ([@SomeHats](https://github.com/SomeHats))
- [tiny] lift theme in style panel [#3170](https://github.com/tldraw/tldraw/pull/3170) ([@steveruizok](https://github.com/steveruizok))
- `tldraw`, `@tldraw/utils`
- Perf: minor drawing speedup [#3464](https://github.com/tldraw/tldraw/pull/3464) ([@steveruizok](https://github.com/steveruizok))
- fixup file helpers [#3130](https://github.com/tldraw/tldraw/pull/3130) ([@SomeHats](https://github.com/SomeHats))
- `@tldraw/editor`, `tldraw`
- WebGL Minimap [#3510](https://github.com/tldraw/tldraw/pull/3510) ([@ds300](https://github.com/ds300))
- textfields: on mobile edit->edit, allow going to empty geo [#3469](https://github.com/tldraw/tldraw/pull/3469) ([@mimecuvalo](https://github.com/mimecuvalo))
- Faster selection / erasing [#3454](https://github.com/tldraw/tldraw/pull/3454) ([@steveruizok](https://github.com/steveruizok))
- Fix SVG exports in Next.js [#3446](https://github.com/tldraw/tldraw/pull/3446) ([@SomeHats](https://github.com/SomeHats))
- Perf: Incremental culled shapes calculation. [#3411](https://github.com/tldraw/tldraw/pull/3411) ([@MitjaBezensek](https://github.com/MitjaBezensek) [@steveruizok](https://github.com/steveruizok))
- Fix some tests [#3403](https://github.com/tldraw/tldraw/pull/3403) ([@steveruizok](https://github.com/steveruizok))
- Add long press event [#3275](https://github.com/tldraw/tldraw/pull/3275) ([@steveruizok](https://github.com/steveruizok))
- textfields: fix regression with Text shape and resizing [#3333](https://github.com/tldraw/tldraw/pull/3333) ([@mimecuvalo](https://github.com/mimecuvalo))
- Add image annotator example [#3147](https://github.com/tldraw/tldraw/pull/3147) ([@SomeHats](https://github.com/SomeHats))
- [fix] Batch tick events [#3181](https://github.com/tldraw/tldraw/pull/3181) ([@steveruizok](https://github.com/steveruizok))
- [tinyish] Simplify / skip some work in Shape [#3176](https://github.com/tldraw/tldraw/pull/3176) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/state`
- [signia] Smart dirty checking of active computeds [#3516](https://github.com/tldraw/tldraw/pull/3516) ([@ds300](https://github.com/ds300))
- [perf] faster signia capture (again) [#3487](https://github.com/tldraw/tldraw/pull/3487) ([@ds300](https://github.com/ds300))
- [perf] faster signia capture [#3471](https://github.com/tldraw/tldraw/pull/3471) ([@ds300](https://github.com/ds300))
- `tldraw`, `@tldraw/validate`
- [fix] allow loading files [#3517](https://github.com/tldraw/tldraw/pull/3517) ([@ds300](https://github.com/ds300))
- `@tldraw/editor`, `@tldraw/tlschema`
- Color tweaks (light and dark mode) [#3486](https://github.com/tldraw/tldraw/pull/3486) ([@steveruizok](https://github.com/steveruizok) [@huppy-bot[bot]](https://github.com/huppy-bot[bot]))
- `@tldraw/editor`
- Stickies: fix sticky note clipping [#3503](https://github.com/tldraw/tldraw/pull/3503) ([@steveruizok](https://github.com/steveruizok))
- css more shapes that need transparent behavior [#3497](https://github.com/tldraw/tldraw/pull/3497) ([@mimecuvalo](https://github.com/mimecuvalo))
- [fix] use page point for pointer [#3476](https://github.com/tldraw/tldraw/pull/3476) ([@ds300](https://github.com/ds300))
- perf: calculate hypoteneuse manually instead of using hypot [#3468](https://github.com/tldraw/tldraw/pull/3468) ([@mimecuvalo](https://github.com/mimecuvalo))
- Cancel pointer velocity while pinching [#3462](https://github.com/tldraw/tldraw/pull/3462) ([@steveruizok](https://github.com/steveruizok))
- Perf: Use a computed cache for masked shape page bounds [#3460](https://github.com/tldraw/tldraw/pull/3460) ([@steveruizok](https://github.com/steveruizok))
- Remove docs for Editor.batch [#3451](https://github.com/tldraw/tldraw/pull/3451) ([@steveruizok](https://github.com/steveruizok))
- Fix panning. [#3445](https://github.com/tldraw/tldraw/pull/3445) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Perf: Improve text outline performance [#3429](https://github.com/tldraw/tldraw/pull/3429) ([@steveruizok](https://github.com/steveruizok))
- Fix text bug on iOS [#3423](https://github.com/tldraw/tldraw/pull/3423) ([@steveruizok](https://github.com/steveruizok))
- Perf: block hit tests while moving camera [#3418](https://github.com/tldraw/tldraw/pull/3418) ([@steveruizok](https://github.com/steveruizok))
- Fix an issue with layers when moving shapes. [#3380](https://github.com/tldraw/tldraw/pull/3380) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- [culling] minimal culled diff with webgl [#3377](https://github.com/tldraw/tldraw/pull/3377) ([@steveruizok](https://github.com/steveruizok))
- put `getCurrentPageId` into a computed [#3378](https://github.com/tldraw/tldraw/pull/3378) ([@steveruizok](https://github.com/steveruizok))
- Don't trigger pointer move on zoom [#3305](https://github.com/tldraw/tldraw/pull/3305) ([@steveruizok](https://github.com/steveruizok))
- Improve performance of culling [#3272](https://github.com/tldraw/tldraw/pull/3272) ([@steveruizok](https://github.com/steveruizok))
- Skip the random ID for regular history entries [#3183](https://github.com/tldraw/tldraw/pull/3183) ([@steveruizok](https://github.com/steveruizok))
- [tiny] Slightly more efficient selection rotated page bounds / page bounds [#3178](https://github.com/tldraw/tldraw/pull/3178) ([@steveruizok](https://github.com/steveruizok))
- [fix] handles [#3177](https://github.com/tldraw/tldraw/pull/3177) ([@steveruizok](https://github.com/steveruizok))
- [fix] Handles extra renders [#3172](https://github.com/tldraw/tldraw/pull/3172) ([@steveruizok](https://github.com/steveruizok))
- [tiny] remove unused shape indicator equality checker [#3171](https://github.com/tldraw/tldraw/pull/3171) ([@steveruizok](https://github.com/steveruizok))
- [fix] Cleanup text measures [#3169](https://github.com/tldraw/tldraw/pull/3169) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/editor`, `@tldraw/tldraw`, `@tldraw/state`, `@tldraw/store`, `tldraw`, `@tldraw/tlschema`, `@tldraw/utils`, `@tldraw/validate`
- New migrations again [#3220](https://github.com/tldraw/tldraw/pull/3220) ([@ds300](https://github.com/ds300) [@steveruizok](https://github.com/steveruizok))
- `@tldraw/store`
- undo devFreeze unintentional commit [#3466](https://github.com/tldraw/tldraw/pull/3466) ([@mimecuvalo](https://github.com/mimecuvalo))
- Fix typo. [#3306](https://github.com/tldraw/tldraw/pull/3306) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Don't double squash [#3182](https://github.com/tldraw/tldraw/pull/3182) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/editor`, `tldraw`, `@tldraw/tlschema`
- Stickies: release candidate [#3249](https://github.com/tldraw/tldraw/pull/3249) ([@steveruizok](https://github.com/steveruizok) [@mimecuvalo](https://github.com/mimecuvalo) [@TodePond](https://github.com/TodePond) [@huppy-bot[bot]](https://github.com/huppy-bot[bot]))
- `@tldraw/editor`, `@tldraw/store`, `tldraw`
- Improve hand dragging with long press [#3432](https://github.com/tldraw/tldraw/pull/3432) ([@steveruizok](https://github.com/steveruizok))
- Perf: (slightly) faster min dist checks [#3401](https://github.com/tldraw/tldraw/pull/3401) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/editor`, `@tldraw/utils`
- Perf: slightly faster `getShapeAtPoint` [#3416](https://github.com/tldraw/tldraw/pull/3416) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/state`, `@tldraw/tlschema`
- Add white migration [#3334](https://github.com/tldraw/tldraw/pull/3334) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/editor`, `@tldraw/state`, `tldraw`
- Fix blur bug in editable text [#3343](https://github.com/tldraw/tldraw/pull/3343) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/editor`, `tldraw`, `@tldraw/utils`
- Input buffering [#3223](https://github.com/tldraw/tldraw/pull/3223) ([@MitjaBezensek](https://github.com/MitjaBezensek) [@steveruizok](https://github.com/steveruizok))
- Fix lag while panning + translating at the same time [#3186](https://github.com/tldraw/tldraw/pull/3186) ([@ds300](https://github.com/ds300) [@steveruizok](https://github.com/steveruizok))
- `tldraw`, `@tldraw/tlschema`
- Add white [#3321](https://github.com/tldraw/tldraw/pull/3321) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/editor`, `@tldraw/tldraw`, `@tldraw/store`, `tldraw`, `@tldraw/tlschema`, `@tldraw/utils`, `@tldraw/validate`
- use native structuredClone on node, cloudflare workers, and in tests [#3166](https://github.com/tldraw/tldraw/pull/3166) ([@si14](https://github.com/si14))
- `@tldraw/editor`, `@tldraw/state`
- [perf] Reinstate render throttling [#3160](https://github.com/tldraw/tldraw/pull/3160) ([@ds300](https://github.com/ds300))
#### 🖥️ tldraw.com Changes
- fix document name alignment [#3559](https://github.com/tldraw/tldraw/pull/3559) ([@SomeHats](https://github.com/SomeHats))
- Fix version [#3521](https://github.com/tldraw/tldraw/pull/3521) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Only show cursor chat button in select mode [#3485](https://github.com/tldraw/tldraw/pull/3485) ([@TodePond](https://github.com/TodePond))
- Allow users to edit the document title by double clicking it even when editing a shape. [#3459](https://github.com/tldraw/tldraw/pull/3459) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Fix cursor chat in context menu. [#3435](https://github.com/tldraw/tldraw/pull/3435) ([@steveruizok](https://github.com/steveruizok))
- Update romanian translations [#3269](https://github.com/tldraw/tldraw/pull/3269) ([@TodePond](https://github.com/TodePond))
- fix document name overflow [#3263](https://github.com/tldraw/tldraw/pull/3263) ([@SomeHats](https://github.com/SomeHats))
- top bar design tweaks [#3205](https://github.com/tldraw/tldraw/pull/3205) ([@SomeHats](https://github.com/SomeHats))
- Update the document title to include the document name. [#3197](https://github.com/tldraw/tldraw/pull/3197) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Remove access token logic. [#3187](https://github.com/tldraw/tldraw/pull/3187) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- [sync] allow connections from v4 clients [#3173](https://github.com/tldraw/tldraw/pull/3173) ([@ds300](https://github.com/ds300))
- `@tldraw/editor`
- Enable document name [#3150](https://github.com/tldraw/tldraw/pull/3150) ([@ds300](https://github.com/ds300))
#### 📖 Documentation changes
- Add releases section to docs [#3564](https://github.com/tldraw/tldraw/pull/3564) ([@SomeHats](https://github.com/SomeHats))
- conditionally use star-history dark theme [#3461](https://github.com/tldraw/tldraw/pull/3461) ([@sunnyzanchi](https://github.com/sunnyzanchi))
- Update font import URL in quick-start.mdx [#3430](https://github.com/tldraw/tldraw/pull/3430) ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))
- Examples: update kbd shortcuts, add actions overrides example [#3330](https://github.com/tldraw/tldraw/pull/3330) ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git) [@steveruizok](https://github.com/steveruizok))
- Tool with child states [#3074](https://github.com/tldraw/tldraw/pull/3074) ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git) [@steveruizok](https://github.com/steveruizok))
- Add inline behaviour example [#3113](https://github.com/tldraw/tldraw/pull/3113) ([@TodePond](https://github.com/TodePond))
- docs: make header fixed instead of sticky [#3228](https://github.com/tldraw/tldraw/pull/3228) ([@mimecuvalo](https://github.com/mimecuvalo))
- fix docs slugs [#3227](https://github.com/tldraw/tldraw/pull/3227) ([@SomeHats](https://github.com/SomeHats))
- docs: work around browser bug with input+scrolling [#3209](https://github.com/tldraw/tldraw/pull/3209) ([@mimecuvalo](https://github.com/mimecuvalo))
- PDF editor example [#3159](https://github.com/tldraw/tldraw/pull/3159) ([@SomeHats](https://github.com/SomeHats))
- fix docs build [#3201](https://github.com/tldraw/tldraw/pull/3201) ([@ds300](https://github.com/ds300))
- [example] culling [#3174](https://github.com/tldraw/tldraw/pull/3174) ([@steveruizok](https://github.com/steveruizok))
- Fix release eliding [#3156](https://github.com/tldraw/tldraw/pull/3156) ([@ds300](https://github.com/ds300))
- [docs] Sync docs deploy with npm deploy [#3153](https://github.com/tldraw/tldraw/pull/3153) ([@ds300](https://github.com/ds300))
- Updated exploded example link from installation page. [#3138](https://github.com/tldraw/tldraw/pull/3138) ([@Kesavaraja](https://github.com/Kesavaraja))
- Make the custom menu examples a bit clearer [#3106](https://github.com/tldraw/tldraw/pull/3106) ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))
- Custom renderer example [#3091](https://github.com/tldraw/tldraw/pull/3091) ([@steveruizok](https://github.com/steveruizok))
- Shape with Migrations [#3078](https://github.com/tldraw/tldraw/pull/3078) ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git) [@steveruizok](https://github.com/steveruizok))
- Fix typo [#3069](https://github.com/tldraw/tldraw/pull/3069) ([@calebeby](https://github.com/calebeby))
- Add custom tool examples [#3064](https://github.com/tldraw/tldraw/pull/3064) ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))
- Fix broken link for shape example [#3046](https://github.com/tldraw/tldraw/pull/3046) ([@lorenzolewis](https://github.com/lorenzolewis))
- Custom shape examples [#2994](https://github.com/tldraw/tldraw/pull/2994) ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git) [@steveruizok](https://github.com/steveruizok))
- Example of using tldraw styles [#3017](https://github.com/tldraw/tldraw/pull/3017) ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git) [@steveruizok](https://github.com/steveruizok))
- `@tldraw/editor`
- Add slides example [#3467](https://github.com/tldraw/tldraw/pull/3467) ([@MitjaBezensek](https://github.com/MitjaBezensek) [@TodePond](https://github.com/TodePond))
- side effects reference docs & examples [#3258](https://github.com/tldraw/tldraw/pull/3258) ([@SomeHats](https://github.com/SomeHats))
- fix docs not building due to typo [#3259](https://github.com/tldraw/tldraw/pull/3259) ([@SomeHats](https://github.com/SomeHats))
- `@tldraw/store`
- Fix typo in Store.ts [#3385](https://github.com/tldraw/tldraw/pull/3385) ([@OrionReed](https://github.com/OrionReed))
- `tldraw`
- docs: fix missing API entries [#3111](https://github.com/tldraw/tldraw/pull/3111) ([@mimecuvalo](https://github.com/mimecuvalo) [@steveruizok](https://github.com/steveruizok))
- `@tldraw/state`
- Fix typo in useValue comment [#3088](https://github.com/tldraw/tldraw/pull/3088) ([@Slowhand0309](https://github.com/Slowhand0309))
#### 🏠 Internal
- Fix deploy script [#3550](https://github.com/tldraw/tldraw/pull/3550) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- [internal] Add license report scripts [#2751](https://github.com/tldraw/tldraw/pull/2751) ([@steveruizok](https://github.com/steveruizok))
- [chore] Bump browser-fs-access. [#3277](https://github.com/tldraw/tldraw/pull/3277) ([@steveruizok](https://github.com/steveruizok))
- log message size in worker analytics [#3274](https://github.com/tldraw/tldraw/pull/3274) ([@SomeHats](https://github.com/SomeHats))
- Add yarn immutable check to pre-commit. [#3218](https://github.com/tldraw/tldraw/pull/3218) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- tooling: prettier ignore pr template [#3210](https://github.com/tldraw/tldraw/pull/3210) ([@mimecuvalo](https://github.com/mimecuvalo))
- Add release docs [#3158](https://github.com/tldraw/tldraw/pull/3158) ([@ds300](https://github.com/ds300))
- Simplify tlsync types [#3139](https://github.com/tldraw/tldraw/pull/3139) ([@si14](https://github.com/si14))
- [DX] PR labels revamp [#3112](https://github.com/tldraw/tldraw/pull/3112) ([@ds300](https://github.com/ds300))
- Restore export menu content [#3126](https://github.com/tldraw/tldraw/pull/3126) ([@steveruizok](https://github.com/steveruizok))
- Don't import package.json in scripts/refresh-assets.ts, just read it [#3116](https://github.com/tldraw/tldraw/pull/3116) ([@si14](https://github.com/si14))
- [dx] Allow vscode to search inside md files by default [#3105](https://github.com/tldraw/tldraw/pull/3105) ([@ds300](https://github.com/ds300))
- Debounce/aggregate tlsync messages [#3012](https://github.com/tldraw/tldraw/pull/3012) ([@si14](https://github.com/si14))
- [infra] Fix patch release script [#3095](https://github.com/tldraw/tldraw/pull/3095) ([@ds300](https://github.com/ds300))
- [infra] Patch release scripting [#3072](https://github.com/tldraw/tldraw/pull/3072) ([@ds300](https://github.com/ds300))
- Shorten url state [#3041](https://github.com/tldraw/tldraw/pull/3041) ([@steveruizok](https://github.com/steveruizok))
- Fix cursor chat bubble position. [#3042](https://github.com/tldraw/tldraw/pull/3042) ([@steveruizok](https://github.com/steveruizok))
- Configure dependabot. [#2980](https://github.com/tldraw/tldraw/pull/2980) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- [infra] fix canary dist tag [#3048](https://github.com/tldraw/tldraw/pull/3048) ([@ds300](https://github.com/ds300))
- `@tldraw/editor`
- Use computed cache for getting the parent child relationships [#3508](https://github.com/tldraw/tldraw/pull/3508) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Perf: Improve perf of `getCurrentPageShapesSorted` [#3453](https://github.com/tldraw/tldraw/pull/3453) ([@MitjaBezensek](https://github.com/MitjaBezensek) [@steveruizok](https://github.com/steveruizok))
- Only run when shapes change. [#3456](https://github.com/tldraw/tldraw/pull/3456) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Reorder dom elements. [#3431](https://github.com/tldraw/tldraw/pull/3431) ([@MitjaBezensek](https://github.com/MitjaBezensek) [@steveruizok](https://github.com/steveruizok))
- fix export preview size [#3264](https://github.com/tldraw/tldraw/pull/3264) ([@SomeHats](https://github.com/SomeHats))
- A few more async routes [#3023](https://github.com/tldraw/tldraw/pull/3023) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/editor`, `tldraw`
- Fix culling. [#3504](https://github.com/tldraw/tldraw/pull/3504) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Revert "RBush again? (#3439)" [#3481](https://github.com/tldraw/tldraw/pull/3481) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- RBush again? [#3439](https://github.com/tldraw/tldraw/pull/3439) ([@MitjaBezensek](https://github.com/MitjaBezensek) [@steveruizok](https://github.com/steveruizok))
- [culling] Improve setting of display none. [#3376](https://github.com/tldraw/tldraw/pull/3376) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- `@tldraw/state`
- Revert "[perf] faster signia capture (#3471)" [#3480](https://github.com/tldraw/tldraw/pull/3480) ([@ds300](https://github.com/ds300))
- `@tldraw/utils`
- Add two simple perf helpers. [#3399](https://github.com/tldraw/tldraw/pull/3399) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- `@tldraw/editor`, `tldraw`, `@tldraw/tlschema`
- Display none for culled shapes [#3291](https://github.com/tldraw/tldraw/pull/3291) ([@MitjaBezensek](https://github.com/MitjaBezensek) [@steveruizok](https://github.com/steveruizok))
- `@tldraw/editor`, `@tldraw/state`, `tldraw`
- Revert perf changes [#3217](https://github.com/tldraw/tldraw/pull/3217) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- `@tldraw/tldraw`
- Remove namespaced-tldraw/tldraw.css [#3068](https://github.com/tldraw/tldraw/pull/3068) ([@SomeHats](https://github.com/SomeHats))
- `@tldraw/tlschema`
- Remove dependabot config since it only controls version updates? [#3057](https://github.com/tldraw/tldraw/pull/3057) ([@MitjaBezensek](https://github.com/MitjaBezensek))
#### 🐛 Bug Fixes
- VS Code 2.0.30 [#3519](https://github.com/tldraw/tldraw/pull/3519) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- [hotfix] Panning fix for VS Code [#3452](https://github.com/tldraw/tldraw/pull/3452) (huppy+SomeHats@tldraw.com huppy+ds300@tldraw.com [@SomeHats](https://github.com/SomeHats) [@web-flow](https://github.com/web-flow) huppy+mimecuvalo@tldraw.com [@steveruizok](https://github.com/steveruizok) [@MitjaBezensek](https://github.com/MitjaBezensek) [@ds300](https://github.com/ds300) huppy+steveruizok@tldraw.com)
- Fix viewport params for pages. [#3079](https://github.com/tldraw/tldraw/pull/3079) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Better websocket reconnection handling [#2960](https://github.com/tldraw/tldraw/pull/2960) ([@si14](https://github.com/si14) [@ds300](https://github.com/ds300))
- Lokalise: Translations update [#3049](https://github.com/tldraw/tldraw/pull/3049) ([@TodePond](https://github.com/TodePond))
- `@tldraw/assets`, `@tldraw/editor`, `tldraw`
- ui: make toasts look more toasty [#2988](https://github.com/tldraw/tldraw/pull/2988) ([@mimecuvalo](https://github.com/mimecuvalo))
- `@tldraw/state`
- Revert throttling of useValue and useStateTracking. [#3129](https://github.com/tldraw/tldraw/pull/3129) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- `tldraw`, `@tldraw/utils`
- chore: cleanup multiple uses of FileReader [#3110](https://github.com/tldraw/tldraw/pull/3110) ([@mimecuvalo](https://github.com/mimecuvalo))
- `tldraw`
- [fix] Rotated crop handle [#3093](https://github.com/tldraw/tldraw/pull/3093) ([@steveruizok](https://github.com/steveruizok))
- Fix an issue where the video size was not drawn correctly [#3047](https://github.com/tldraw/tldraw/pull/3047) ([@bubweiser](https://github.com/bubweiser) [@steveruizok](https://github.com/steveruizok))
- [fix] Input tags [#3038](https://github.com/tldraw/tldraw/pull/3038) ([@steveruizok](https://github.com/steveruizok))
- [terrible] Firefox: Allow scrolling on keyboard shortcuts dialog [#2974](https://github.com/tldraw/tldraw/pull/2974) ([@TodePond](https://github.com/TodePond) [@steveruizok](https://github.com/steveruizok))
- [fix] Missing element crash (rare) on video shapes. [#3037](https://github.com/tldraw/tldraw/pull/3037) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/editor`, `tldraw`
- Fix validation errors for `duplicateProps` [#3065](https://github.com/tldraw/tldraw/pull/3065) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- children: any -> children: ReactNode [#3061](https://github.com/tldraw/tldraw/pull/3061) ([@SomeHats](https://github.com/SomeHats))
- `@tldraw/editor`, `tldraw`, `@tldraw/utils`
- Wrap local/session storage calls in try/catch (take 2) [#3066](https://github.com/tldraw/tldraw/pull/3066) ([@SomeHats](https://github.com/SomeHats))
- Revert "Protect local storage calls (#3043)" [#3063](https://github.com/tldraw/tldraw/pull/3063) ([@SomeHats](https://github.com/SomeHats))
- Protect local storage calls [#3043](https://github.com/tldraw/tldraw/pull/3043) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/editor`
- Expose `getStyleForNextShape` [#3039](https://github.com/tldraw/tldraw/pull/3039) ([@steveruizok](https://github.com/steveruizok))
- [bugfix] Avoid randomness at init time to allow running on cloudflare. [#3016](https://github.com/tldraw/tldraw/pull/3016) ([@ds300](https://github.com/ds300))
- `@tldraw/editor`, `@tldraw/tldraw`, `tldraw`
- Show a broken image for files without assets [#2990](https://github.com/tldraw/tldraw/pull/2990) ([@steveruizok](https://github.com/steveruizok))
#### 🧹 Chores
- VS Code 2.0.29 [#3515](https://github.com/tldraw/tldraw/pull/3515) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- VS Code 2.0.27 [#3442](https://github.com/tldraw/tldraw/pull/3442) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- VS Code 2.0.26 [#3148](https://github.com/tldraw/tldraw/pull/3148) ([@MitjaBezensek](https://github.com/MitjaBezensek))
#### 🧪 Tests
- attempted fix of a flaky ClientWebSocketAdapter test [#3114](https://github.com/tldraw/tldraw/pull/3114) ([@si14](https://github.com/si14))
- `@tldraw/editor`
- Add tests for Vec.Average [#3071](https://github.com/tldraw/tldraw/pull/3071) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- `@tldraw/editor`, `@tldraw/tlschema`
- [fix] Routes check on e2e tests [#3022](https://github.com/tldraw/tldraw/pull/3022) ([@steveruizok](https://github.com/steveruizok))
#### 🔩 Dependency Updates
- Bump the npm_and_yarn group across 1 directory with 2 updates [#3505](https://github.com/tldraw/tldraw/pull/3505) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@mimecuvalo](https://github.com/mimecuvalo) [@steveruizok](https://github.com/steveruizok))
- Bump the npm_and_yarn group across 1 directory with 2 updates [#3443](https://github.com/tldraw/tldraw/pull/3443) ([@dependabot[bot]](https://github.com/dependabot[bot]))
- Bump the npm_and_yarn group across 1 directory with 1 update [#3348](https://github.com/tldraw/tldraw/pull/3348) ([@dependabot[bot]](https://github.com/dependabot[bot]))
- Bump the npm_and_yarn group across 1 directory with 2 updates [#3304](https://github.com/tldraw/tldraw/pull/3304) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@github-actions[bot]](https://github.com/github-actions[bot]) [@MitjaBezensek](https://github.com/MitjaBezensek))
- Bump the npm_and_yarn group across 1 directory with 2 updates [#3165](https://github.com/tldraw/tldraw/pull/3165) ([@dependabot[bot]](https://github.com/dependabot[bot]))
#### Authors: 23
- [@dependabot[bot]](https://github.com/dependabot[bot])
- [@github-actions[bot]](https://github.com/github-actions[bot])
- [@huppy-bot[bot]](https://github.com/huppy-bot[bot])
- alex ([@SomeHats](https://github.com/SomeHats))
- Caleb Eby ([@calebeby](https://github.com/calebeby))
- Dan Groshev ([@si14](https://github.com/si14))
- David Sheldrick ([@ds300](https://github.com/ds300))
- ds300 (huppy+ds300@tldraw.com)
- GitHub Web Flow ([@web-flow](https://github.com/web-flow))
- hirano ([@bubweiser](https://github.com/bubweiser))
- Kesavaraja Krishnan ([@Kesavaraja](https://github.com/Kesavaraja))
- Lorenzo Lewis ([@lorenzolewis](https://github.com/lorenzolewis))
- Lu Wilson ([@TodePond](https://github.com/TodePond))
- Mime Čuvalo ([@mimecuvalo](https://github.com/mimecuvalo))
- mimecuvalo (huppy+mimecuvalo@tldraw.com)
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Orion Reed ([@OrionReed](https://github.com/OrionReed))
- Slowhand ([@Slowhand0309](https://github.com/Slowhand0309))
- SomeHats (huppy+SomeHats@tldraw.com)
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
- steveruizok (huppy+steveruizok@tldraw.com)
- Sunny Zanchi ([@sunnyzanchi](https://github.com/sunnyzanchi))
- Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))
### [v2.0.2](/releases/v2.0.2)
#### 🐛 Bug Fix

Wyświetl plik

@ -1,24 +0,0 @@
---
title: v2.0.0
description: Examples
author: tldraw
date: 2/29/2024
order: 2
status: published
---
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0)
#### ⚠️ Pushed to `main`
- `@tldraw/tldraw`, `tldraw`
- updatereadmes ([@steveruizok](https://github.com/steveruizok))
#### 📝 Documentation
- `@tldraw/tldraw`
- Update readmes / docs for 2.0 [#3011](https://github.com/tldraw/tldraw/pull/3011) ([@steveruizok](https://github.com/steveruizok))
#### Authors: 1
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok))

Wyświetl plik

@ -1,24 +0,0 @@
---
title: v2.0.1
description: Examples
author: tldraw
date: 3/11/2024
order: 1
status: published
---
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.1)
#### 🐛 Bug Fix
- `@tldraw/editor`
- [patch 2.0.1] Cherry-pick 'Avoid randomness at init time...' [#3076](https://github.com/tldraw/tldraw/pull/3076) ([@ds300](https://github.com/ds300))
#### ⚠️ Pushed to `v2.0.x`
- fetch main during patch publish ([@ds300](https://github.com/ds300))
- cherry-pick tooling changes too i guess ([@ds300](https://github.com/ds300))
#### Authors: 1
- David Sheldrick ([@ds300](https://github.com/ds300))

Wyświetl plik

@ -1,26 +0,0 @@
---
title: v2.0.2
description: Examples
author: tldraw
date: 3/18/2024
order: 0
status: published
---
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.2)
#### 🐛 Bug Fix
- [docs] Sync docs deploy with npm deploy [#3153](https://github.com/tldraw/tldraw/pull/3153) ([@ds300](https://github.com/ds300))
- `tldraw`
- Fix jpg export and tests (#3198) [#3199](https://github.com/tldraw/tldraw/pull/3199) ([@SomeHats](https://github.com/SomeHats))
#### ⚠️ Pushed to `v2.0.x`
- empty commit ([@SomeHats](https://github.com/SomeHats))
- fix release eliding ([@ds300](https://github.com/ds300))
#### Authors: 2
- alex ([@SomeHats](https://github.com/SomeHats))
- David Sheldrick ([@ds300](https://github.com/ds300))

Wyświetl plik

@ -29,8 +29,8 @@
"dev": "concurrently \"NODE_ENV=development next dev --port=3001\" \"tsx ./watcher.ts\" --kill-others",
"next-dev": "next dev",
"lint": "yarn run -T tsx ../../scripts/lint.ts",
"build": "yarn create-api-markdown && yarn refresh-content && next build && yarn check-links",
"start": "yarn create-api-markdown && yarn refresh-content && next start",
"build": "yarn refresh-everything && next build && yarn check-links",
"start": "yarn refresh-everything && next start",
"fetch-api-source": "yarn run -T tsx --tsconfig ./tsconfig.content.json ./scripts/fetch-api-source.ts",
"fetch-releases": "yarn run -T tsx --tsconfig ./tsconfig.content.json ./scripts/fetch-releases.ts",
"check-links": "yarn run -T tsx --tsconfig ./tsconfig.content.json ./scripts/check-broken-links.ts",

Wyświetl plik

@ -2,10 +2,14 @@ import path from 'path'
import { open } from 'sqlite'
import sqlite3 from 'sqlite3'
export async function connect(opts = {} as { reset?: boolean }) {
export async function connect(opts: { reset?: boolean; mode: 'readonly' | 'readwrite' }) {
const db = await open({
filename: path.join(process.cwd(), 'content.db'),
driver: sqlite3.Database,
mode:
opts.mode === 'readonly'
? sqlite3.OPEN_READONLY
: sqlite3.OPEN_CREATE | sqlite3.OPEN_READWRITE,
})
if (opts.reset) {

Wyświetl plik

@ -9,7 +9,7 @@ import { generateExamplesContent } from './generateExamplesContent'
export async function refreshContent(opts = {} as { silent: boolean }) {
if (!opts.silent) nicelog('◦ Resetting database...')
const db = await connect({ reset: true })
const db = await connect({ reset: true, mode: 'readwrite' })
if (!opts.silent) nicelog('◦ Adding authors to db...')
await addAuthors(db, await require('../../content/authors.json'))

Wyświetl plik

@ -2,7 +2,7 @@ import { nicelog } from '@/utils/nicelog'
import { connect } from './functions/connect'
;(async function () {
nicelog('◦ Resetting database...')
await connect({ reset: true })
await connect({ reset: true, mode: 'readwrite' })
nicelog('✔ Complete!')
process.exit()
})()

Wyświetl plik

@ -295,9 +295,9 @@ export class ContentDatabase {
let contentDatabase: ContentDatabase | null = null
export async function getDb(opts = {} as { reset?: boolean }) {
if (!contentDatabase || opts.reset) {
const db = await connect(opts)
export async function getDb() {
if (!contentDatabase) {
const db = await connect({ mode: 'readonly' })
contentDatabase = new ContentDatabase(db)
}

Wyświetl plik

@ -210,7 +210,7 @@ export async function getVectorDb(
if (opts.updateContent || opts.rebuildIndex) {
nicelog(`Rebuilding index`)
const db = await connect({ reset: false })
const db = await connect({ reset: false, mode: 'readonly' })
nicelog(`Getting articles`)
const articles =

Wyświetl plik

@ -23,6 +23,7 @@
"dependencies": {
"@supabase/auth-helpers-remix": "^0.2.2",
"@supabase/supabase-js": "^2.33.2",
"@tldraw/dotcom-shared": "workspace:*",
"@tldraw/store": "workspace:*",
"@tldraw/tlschema": "workspace:*",
"@tldraw/tlsync": "workspace:*",

Wyświetl plik

@ -3,7 +3,16 @@
import { SupabaseClient } from '@supabase/supabase-js'
import {
READ_ONLY_LEGACY_PREFIX,
READ_ONLY_PREFIX,
ROOM_OPEN_MODE,
ROOM_PREFIX,
type RoomOpenMode,
} from '@tldraw/dotcom-shared'
import {
DBLoadResultType,
RoomSnapshot,
TLCloseEventCode,
TLServer,
TLServerEvent,
TLSyncRoom,
@ -19,6 +28,7 @@ import { PERSIST_INTERVAL_MS } from './config'
import { getR2KeyForRoom } from './r2'
import { Analytics, Environment } from './types'
import { createSupabaseClient } from './utils/createSupabaseClient'
import { getSlug } from './utils/roomOpenMode'
import { throttle } from './utils/throttle'
const MAX_CONNECTIONS = 50
@ -87,13 +97,23 @@ export class TLDrawDurableObject extends TLServer {
readonly router = Router()
.get(
'/r/:roomId',
(req) => this.extractDocumentInfoFromRequest(req),
`/${ROOM_PREFIX}/:roomId`,
(req) => this.extractDocumentInfoFromRequest(req, ROOM_OPEN_MODE.READ_WRITE),
(req) => this.onRequest(req)
)
.get(
`/${READ_ONLY_LEGACY_PREFIX}/:roomId`,
(req) => this.extractDocumentInfoFromRequest(req, ROOM_OPEN_MODE.READ_ONLY_LEGACY),
(req) => this.onRequest(req)
)
.get(
`/${READ_ONLY_PREFIX}/:roomId`,
(req) => this.extractDocumentInfoFromRequest(req, ROOM_OPEN_MODE.READ_ONLY),
(req) => this.onRequest(req)
)
.post(
'/r/:roomId/restore',
(req) => this.extractDocumentInfoFromRequest(req),
`/${ROOM_PREFIX}/:roomId/restore`,
(req) => this.extractDocumentInfoFromRequest(req, ROOM_OPEN_MODE.READ_WRITE),
(req) => this.onRestore(req)
)
.all('*', () => new Response('Not found', { status: 404 }))
@ -113,8 +133,11 @@ export class TLDrawDurableObject extends TLServer {
get documentInfo() {
return assertExists(this._documentInfo, 'documentInfo must be present')
}
extractDocumentInfoFromRequest = async (req: IRequest) => {
const slug = assertExists(req.params.roomId, 'roomId must be present')
extractDocumentInfoFromRequest = async (req: IRequest, roomOpenMode: RoomOpenMode) => {
const slug = assertExists(
await getSlug(this.env, req.params.roomId, roomOpenMode),
'roomId must be present'
)
if (this._documentInfo) {
assert(this._documentInfo.slug === slug, 'slug must match')
} else {
@ -226,9 +249,10 @@ export class TLDrawDurableObject extends TLServer {
const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair()
// Handle the connection (see TLServer)
let connectionResult: DBLoadResultType
try {
// block concurrency while initializing the room if that needs to happen
await this.controller.blockConcurrencyWhile(() =>
connectionResult = await this.controller.blockConcurrencyWhile(() =>
this.handleConnection({
socket: serverWebSocket as any,
persistenceKey: this.documentInfo.slug!,
@ -253,6 +277,12 @@ export class TLDrawDurableObject extends TLServer {
this.schedulePersist()
})
if (connectionResult === 'room_not_found') {
// If the room is not found, we need to accept and then immediately close the connection
// with our custom close code.
serverWebSocket.close(TLCloseEventCode.NOT_FOUND, 'Room not found')
}
return new Response(null, { status: 101, webSocket: clientWebSocket })
}

Wyświetl plik

@ -1,24 +1,22 @@
import { SerializedSchema, SerializedStore } from '@tldraw/store'
import { TLRecord } from '@tldraw/tlschema'
import { CreateRoomRequestBody } from '@tldraw/dotcom-shared'
import { RoomSnapshot, schema } from '@tldraw/tlsync'
import { IRequest } from 'itty-router'
import { nanoid } from 'nanoid'
import { getR2KeyForRoom } from '../r2'
import { Environment } from '../types'
import { validateSnapshot } from '../utils/validateSnapshot'
type SnapshotRequestBody = {
schema: SerializedSchema
snapshot: SerializedStore<TLRecord>
}
import { isAllowedOrigin } from '../worker'
// Sets up a new room based on a provided snapshot, e.g. when a user clicks the "Share" buttons or the "Fork project" buttons.
export async function createRoom(request: IRequest, env: Environment): Promise<Response> {
// The data sent from the client will include the data for the new room
const data = (await request.json()) as SnapshotRequestBody
const data = (await request.json()) as CreateRoomRequestBody
if (!isAllowedOrigin(data.origin)) {
return Response.json({ error: true, message: 'Not allowed' }, { status: 406 })
}
// There's a chance the data will be invalid, so we check it first
const snapshotResult = validateSnapshot(data)
const snapshotResult = validateSnapshot(data.snapshot)
if (!snapshotResult.ok) {
return Response.json({ error: true, message: snapshotResult.error }, { status: 400 })
}
@ -40,6 +38,11 @@ export async function createRoom(request: IRequest, env: Environment): Promise<R
// Bang that snapshot into the database
await env.ROOMS.put(getR2KeyForRoom(slug), JSON.stringify(snapshot))
// Create a readonly slug and store it
const readonlySlug = nanoid()
await env.SLUG_TO_READONLY_SLUG.put(slug, readonlySlug)
await env.READONLY_SLUG_TO_SLUG.put(readonlySlug, slug)
// Send back the slug so that the client can redirect to the new room
return new Response(JSON.stringify({ error: false, slug }))
}

Wyświetl plik

@ -1,5 +1,4 @@
import { SerializedSchema, SerializedStore } from '@tldraw/store'
import { TLRecord } from '@tldraw/tlschema'
import { CreateSnapshotRequestBody } from '@tldraw/dotcom-shared'
import { IRequest } from 'itty-router'
import { nanoid } from 'nanoid'
import { Environment } from '../types'
@ -7,12 +6,6 @@ import { createSupabaseClient, noSupabaseSorry } from '../utils/createSupabaseCl
import { getSnapshotsTable } from '../utils/getSnapshotsTable'
import { validateSnapshot } from '../utils/validateSnapshot'
type CreateSnapshotRequestBody = {
schema: SerializedSchema
snapshot: SerializedStore<TLRecord>
parent_slug?: string | string[] | undefined
}
export async function createRoomSnapshot(request: IRequest, env: Environment): Promise<Response> {
const data = (await request.json()) as CreateSnapshotRequestBody

Wyświetl plik

@ -1,3 +1,4 @@
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
import { IRequest } from 'itty-router'
import { Environment } from '../types'
import { fourOhFour } from '../utils/fourOhFour'
@ -11,6 +12,6 @@ export async function forwardRoomRequest(request: IRequest, env: Environment): P
if (isRoomIdTooLong(roomId)) return roomIdIsTooLong()
// Set up the durable object for this room
const id = env.TLDR_DOC.idFromName(`/r/${roomId}`)
const id = env.TLDR_DOC.idFromName(`/${ROOM_PREFIX}/${roomId}`)
return env.TLDR_DOC.get(id).fetch(request)
}

Wyświetl plik

@ -0,0 +1,30 @@
import { GetReadonlySlugResponseBody } from '@tldraw/dotcom-shared'
import { lns } from '@tldraw/utils'
import { IRequest } from 'itty-router'
import { Environment } from '../types'
// Return a URL to a readonly version of the room
export async function getReadonlySlug(request: IRequest, env: Environment): Promise<Response> {
const roomId = request.params.roomId
if (!roomId) {
return new Response('Bad request', {
status: 400,
})
}
let slug = await env.SLUG_TO_READONLY_SLUG.get(roomId)
let isLegacy = false
if (!slug) {
// For all newly created rooms we add the readonly slug to the KV store.
// If it does not exist there it means we are trying to get a slug for an old room.
slug = lns(roomId)
isLegacy = true
}
return new Response(
JSON.stringify({
slug,
isLegacy,
} satisfies GetReadonlySlugResponseBody)
)
}

Wyświetl plik

@ -1,18 +1,23 @@
import { ROOM_PREFIX, RoomOpenMode } from '@tldraw/dotcom-shared'
import { IRequest } from 'itty-router'
import { Environment } from '../types'
import { fourOhFour } from '../utils/fourOhFour'
import { isRoomIdTooLong, roomIdIsTooLong } from '../utils/roomIdIsTooLong'
import { getSlug } from '../utils/roomOpenMode'
// This is the entry point for joining an existing room
export async function joinExistingRoom(request: IRequest, env: Environment): Promise<Response> {
const roomId = request.params.roomId
export async function joinExistingRoom(
request: IRequest,
env: Environment,
roomOpenMode: RoomOpenMode
): Promise<Response> {
const roomId = await getSlug(env, request.params.roomId, roomOpenMode)
if (!roomId) return fourOhFour()
if (isRoomIdTooLong(roomId)) return roomIdIsTooLong()
// This needs to be a websocket request!
if (request.headers.get('upgrade')?.toLowerCase() === 'websocket') {
// Set up the durable object for this room
const id = env.TLDR_DOC.idFromName(`/r/${roomId}`)
const id = env.TLDR_DOC.idFromName(`/${ROOM_PREFIX}/${roomId}`)
return env.TLDR_DOC.get(id).fetch(request)
}

Wyświetl plik

@ -17,6 +17,9 @@ export interface Environment {
ROOMS: R2Bucket
ROOMS_HISTORY_EPHEMERAL: R2Bucket
SLUG_TO_READONLY_SLUG: KVNamespace
READONLY_SLUG_TO_SLUG: KVNamespace
// env vars
SUPABASE_URL: string | undefined
SUPABASE_KEY: string | undefined

Wyświetl plik

@ -0,0 +1,17 @@
import { ROOM_OPEN_MODE, RoomOpenMode } from '@tldraw/dotcom-shared'
import { exhaustiveSwitchError, lns } from '@tldraw/utils'
import { Environment } from '../types'
export async function getSlug(env: Environment, slug: string | null, roomOpenMode: RoomOpenMode) {
if (!slug) return null
switch (roomOpenMode) {
case ROOM_OPEN_MODE.READ_WRITE:
return slug
case ROOM_OPEN_MODE.READ_ONLY:
return await env.READONLY_SLUG_TO_SLUG.get(slug)
case ROOM_OPEN_MODE.READ_ONLY_LEGACY:
return lns(slug)
default:
exhaustiveSwitchError(roomOpenMode)
}
}

Wyświetl plik

@ -1,11 +1,18 @@
/// <reference no-default-lib="true"/>
/// <reference types="@cloudflare/workers-types" />
import {
READ_ONLY_LEGACY_PREFIX,
READ_ONLY_PREFIX,
ROOM_OPEN_MODE,
ROOM_PREFIX,
} from '@tldraw/dotcom-shared'
import { Router, createCors } from 'itty-router'
import { env } from 'process'
import Toucan from 'toucan-js'
import { createRoom } from './routes/createRoom'
import { createRoomSnapshot } from './routes/createRoomSnapshot'
import { forwardRoomRequest } from './routes/forwardRoomRequest'
import { getReadonlySlug } from './routes/getReadonlySlug'
import { getRoomHistory } from './routes/getRoomHistory'
import { getRoomHistorySnapshot } from './routes/getRoomHistorySnapshot'
import { getRoomSnapshot } from './routes/getRoomSnapshot'
@ -24,10 +31,19 @@ const router = Router()
.post('/new-room', createRoom)
.post('/snapshots', createRoomSnapshot)
.get('/snapshot/:roomId', getRoomSnapshot)
.get('/r/:roomId', joinExistingRoom)
.get('/r/:roomId/history', getRoomHistory)
.get('/r/:roomId/history/:timestamp', getRoomHistorySnapshot)
.post('/r/:roomId/restore', forwardRoomRequest)
.get(`/${ROOM_PREFIX}/:roomId`, (req, env) =>
joinExistingRoom(req, env, ROOM_OPEN_MODE.READ_WRITE)
)
.get(`/${READ_ONLY_LEGACY_PREFIX}/:roomId`, (req, env) =>
joinExistingRoom(req, env, ROOM_OPEN_MODE.READ_ONLY_LEGACY)
)
.get(`/${READ_ONLY_PREFIX}/:roomId`, (req, env) =>
joinExistingRoom(req, env, ROOM_OPEN_MODE.READ_ONLY)
)
.get(`/${ROOM_PREFIX}/:roomId/history`, getRoomHistory)
.get(`/${ROOM_PREFIX}/:roomId/history/:timestamp`, getRoomHistorySnapshot)
.get('/readonly-slug/:roomId', getReadonlySlug)
.post(`/${ROOM_PREFIX}/:roomId/restore`, forwardRoomRequest)
.all('*', fourOhFour)
const Worker = {
@ -70,7 +86,7 @@ const Worker = {
},
}
function isAllowedOrigin(origin: string) {
export function isAllowedOrigin(origin: string) {
if (origin === 'http://localhost:3000') return true
if (origin === 'http://localhost:5420') return true
if (origin.endsWith('.tldraw.com')) return true

Wyświetl plik

@ -7,6 +7,9 @@
"emitDeclarationOnly": false
},
"references": [
{
"path": "../../packages/dotcom-shared"
},
{
"path": "../../packages/store"
},

Wyświetl plik

@ -114,3 +114,36 @@ bucket_name = "rooms-history-ephemeral-preview"
[[env.production.r2_buckets]]
binding = "ROOMS_HISTORY_EPHEMERAL"
bucket_name = "rooms-history-ephemeral"
#################### Key value storage ####################
[[env.dev.kv_namespaces]]
binding = "SLUG_TO_READONLY_SLUG"
id = "847a6bded62045c6808dda6a275ef96c"
[[env.dev.kv_namespaces]]
binding = "READONLY_SLUG_TO_SLUG"
id = "0a83acab40374ccd918cc9d755741714"
[[env.preview.kv_namespaces]]
binding = "SLUG_TO_READONLY_SLUG"
id = "847a6bded62045c6808dda6a275ef96c"
[[env.preview.kv_namespaces]]
binding = "READONLY_SLUG_TO_SLUG"
id = "0a83acab40374ccd918cc9d755741714"
[[env.staging.kv_namespaces]]
binding = "SLUG_TO_READONLY_SLUG"
id = "847a6bded62045c6808dda6a275ef96c"
[[env.staging.kv_namespaces]]
binding = "READONLY_SLUG_TO_SLUG"
id = "0a83acab40374ccd918cc9d755741714"
[[env.production.kv_namespaces]]
binding = "SLUG_TO_READONLY_SLUG"
id = "2fb5fc7f7ca54a5a9dfae1b07a30a778"
[[env.production.kv_namespaces]]
binding = "READONLY_SLUG_TO_SLUG"
id = "96be6637b281412ab35b2544539d78e8"

Wyświetl plik

@ -23,6 +23,7 @@
"@sentry/integrations": "^7.34.0",
"@sentry/react": "^7.77.0",
"@tldraw/assets": "workspace:*",
"@tldraw/dotcom-shared": "workspace:*",
"@tldraw/tlsync": "workspace:*",
"@tldraw/utils": "workspace:*",
"@vercel/analytics": "^1.1.1",

Wyświetl plik

@ -8,6 +8,7 @@ import json5 from 'json5'
import { nicelog } from '../../../scripts/lib/nicelog'
import { T } from '@tldraw/validate'
import { getMultiplayerServerURL } from '../vite.config'
// We load the list of routes that should be forwarded to our SPA's index.html here.
// It uses a jest snapshot file because deriving the set of routes from our
@ -56,9 +57,7 @@ async function build() {
// rewrite api calls to the multiplayer server
{
src: '^/api(/(.*))?$',
dest: `${
process.env.MULTIPLAYER_SERVER?.replace(/^ws/, 'http') ?? 'http://127.0.0.1:8787'
}$1`,
dest: `${getMultiplayerServerURL()}$1`,
check: true,
},
// cache static assets immutably

Wyświetl plik

@ -26,6 +26,10 @@ exports[`the_routes 1`] = `
"reactRouterPattern": "/r/:roomId",
"vercelRouterPattern": "^/r/[^/]*/?$",
},
{
"reactRouterPattern": "/ro/:roomId",
"vercelRouterPattern": "^/ro/[^/]*/?$",
},
{
"reactRouterPattern": "/s/:roomId",
"vercelRouterPattern": "^/s/[^/]*/?$",

Wyświetl plik

@ -1,3 +1,4 @@
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
import { RoomSnapshot } from '@tldraw/tlsync'
import { useCallback, useState } from 'react'
import { Tldraw, createTLStore, defaultShapeUtils } from 'tldraw'
@ -31,7 +32,7 @@ export function BoardHistorySnapshot({
const sure = window.confirm('Are you sure?')
if (!sure) return
const res = await fetch(`/api/r/${roomId}/restore`, {
const res = await fetch(`/api/${ROOM_PREFIX}/${roomId}/restore`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

Wyświetl plik

@ -1,4 +1,5 @@
import { Link } from 'react-router-dom'
import { isInIframe } from '../../utils/iFrame'
export function ErrorPage({
icon,
@ -6,8 +7,8 @@ export function ErrorPage({
}: {
icon?: boolean
messages: { header: string; para1: string; para2?: string }
redirectTo?: string
}) {
const inIframe = isInIframe()
return (
<div className="error-page">
<div className="error-page__container">
@ -19,8 +20,8 @@ export function ErrorPage({
<p>{messages.para1}</p>
{messages.para2 && <p>{messages.para2}</p>}
</div>
<Link to={'/'}>
<a>Take me home.</a>
<Link to={'/'} target={inIframe ? '_blank' : '_self'}>
{inIframe ? 'Open tldraw.' : 'Back to tldraw.'}
</Link>
</div>
</div>

Wyświetl plik

@ -2,12 +2,13 @@ import { ReactNode, useEffect, useState } from 'react'
import { LoadingScreen } from 'tldraw'
import { version } from '../../version'
import { useUrl } from '../hooks/useUrl'
import { getParentOrigin, isInIframe } from '../utils/iFrame'
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
/*
If we're in an iframe, we need to figure out whether we're on a whitelisted host (e.g. tldraw itself)
or a not-allowed host (e.g. someone else's website). Some websites embed tldraw in iframes and this is kinda
risky for us and for them, tooand hey, if we decide to offer a hosted thing, then that's another stor
risky for us and for them, tooand hey, if we decide to offer a hosted thing, then that's another story.
Figuring this out is a little tricky because the same code here is going to run on:
- the website as a top window (tldraw-top)
@ -26,33 +27,45 @@ and we should show an annoying messsage.
If we're not in an iframe, we don't need to do anything.
*/
export const ROOM_CONTEXT = {
PUBLIC_MULTIPLAYER: 'public-multiplayer',
PUBLIC_READONLY: 'public-readonly',
PUBLIC_SNAPSHOT: 'public-snapshot',
HISTORY_SNAPSHOT: 'history-snapshot',
HISTORY: 'history',
LOCAL: 'local',
} as const
type $ROOM_CONTEXT = (typeof ROOM_CONTEXT)[keyof typeof ROOM_CONTEXT]
const EMBEDDED_STATE = {
IFRAME_UNKNOWN: 'iframe-unknown',
IFRAME_NOT_ALLOWED: 'iframe-not-allowed',
NOT_IFRAME: 'not-iframe',
IFRAME_OK: 'iframe-ok',
} as const
type $EMBEDDED_STATE = (typeof EMBEDDED_STATE)[keyof typeof EMBEDDED_STATE]
// Which routes do we allow to be embedded in tldraw.com itself?
const WHITELIST_CONTEXT = ['public-multiplayer', 'public-readonly', 'public-snapshot']
const WHITELIST_CONTEXT: $ROOM_CONTEXT[] = [
ROOM_CONTEXT.PUBLIC_MULTIPLAYER,
ROOM_CONTEXT.PUBLIC_READONLY,
ROOM_CONTEXT.PUBLIC_SNAPSHOT,
]
const EXPECTED_QUESTION = 'are we cool?'
const EXPECTED_RESPONSE = 'yes' + version
const isInIframe = () => {
return typeof window !== 'undefined' && (window !== window.top || window.self !== window.parent)
}
export function IFrameProtector({
slug,
context,
children,
}: {
slug: string
context:
| 'public-multiplayer'
| 'public-readonly'
| 'public-snapshot'
| 'history-snapshot'
| 'history'
| 'local'
context: $ROOM_CONTEXT
children: ReactNode
}) {
const [embeddedState, setEmbeddedState] = useState<
'iframe-unknown' | 'iframe-not-allowed' | 'not-iframe' | 'iframe-ok'
>(isInIframe() ? 'iframe-unknown' : 'not-iframe')
const [embeddedState, setEmbeddedState] = useState<$EMBEDDED_STATE>(
isInIframe() ? EMBEDDED_STATE.IFRAME_UNKNOWN : EMBEDDED_STATE.NOT_IFRAME
)
const url = useUrl()
@ -76,24 +89,28 @@ export function IFrameProtector({
if (event.data === EXPECTED_RESPONSE) {
// todo: check the origin?
setEmbeddedState('iframe-ok')
setEmbeddedState(EMBEDDED_STATE.IFRAME_OK)
clearTimeout(timeout)
}
}
window.addEventListener('message', handleMessageEvent, false)
if (embeddedState === 'iframe-unknown') {
if (embeddedState === EMBEDDED_STATE.IFRAME_UNKNOWN) {
// We iframe embeddings on multiplayer or readonly
if (WHITELIST_CONTEXT.includes(context)) {
window.parent.postMessage(EXPECTED_QUESTION, '*') // todo: send to a specific origin?
timeout = setTimeout(() => {
setEmbeddedState('iframe-not-allowed')
trackAnalyticsEvent('connect_to_room_in_iframe', { slug, context })
setEmbeddedState(EMBEDDED_STATE.IFRAME_NOT_ALLOWED)
trackAnalyticsEvent('connect_to_room_in_iframe', {
slug,
context,
origin: getParentOrigin(),
})
}, 1000)
} else {
// We don't allow iframe embeddings on other routes
setEmbeddedState('iframe-not-allowed')
setEmbeddedState(EMBEDDED_STATE.IFRAME_NOT_ALLOWED)
}
}
@ -103,12 +120,12 @@ export function IFrameProtector({
}
}, [embeddedState, slug, context])
if (embeddedState === 'iframe-unknown') {
if (embeddedState === EMBEDDED_STATE.IFRAME_UNKNOWN) {
// We're in an iframe, but we don't know if it's a tldraw iframe
return <LoadingScreen>Loading in an iframe...</LoadingScreen>
return <LoadingScreen>Loading in an iframe</LoadingScreen>
}
if (embeddedState === 'iframe-not-allowed') {
if (embeddedState === EMBEDDED_STATE.IFRAME_NOT_ALLOWED) {
// We're in an iframe and its not one of ours
return (
<div className="tldraw__editor tl-container">

Wyświetl plik

@ -1,3 +1,4 @@
import { ROOM_OPEN_MODE, RoomOpenModeToPath, type RoomOpenMode } from '@tldraw/dotcom-shared'
import { useCallback, useEffect } from 'react'
import {
DefaultContextMenu,
@ -18,7 +19,6 @@ import {
TldrawUiMenuItem,
ViewSubmenu,
atom,
lns,
useActions,
useValue,
} from 'tldraw'
@ -104,19 +104,17 @@ const components: TLComponents = {
}
export function MultiplayerEditor({
isReadOnly,
roomOpenMode,
roomSlug,
}: {
isReadOnly: boolean
roomOpenMode: RoomOpenMode
roomSlug: string
}) {
const handleUiEvent = useHandleUiEvents()
const roomId = isReadOnly ? lns(roomSlug) : roomSlug
const storeWithStatus = useRemoteSyncClient({
uri: `${MULTIPLAYER_SERVER}/r/${roomId}`,
roomId,
uri: `${MULTIPLAYER_SERVER}/${RoomOpenModeToPath[roomOpenMode]}/${roomSlug}`,
roomId: roomSlug,
})
const isOffline =
@ -128,16 +126,22 @@ export function MultiplayerEditor({
const sharingUiOverrides = useSharing()
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
const cursorChatOverrides = useCursorChat()
const isReadonly =
roomOpenMode === ROOM_OPEN_MODE.READ_ONLY || roomOpenMode === ROOM_OPEN_MODE.READ_ONLY_LEGACY
const handleMount = useCallback(
(editor: Editor) => {
;(window as any).app = editor
;(window as any).editor = editor
editor.updateInstanceState({ isReadonly: isReadOnly })
if (!isReadonly) {
;(window as any).app = editor
;(window as any).editor = editor
}
editor.updateInstanceState({
isReadonly,
})
editor.registerExternalAssetHandler('file', createAssetFromFile)
editor.registerExternalAssetHandler('url', createAssetFromUrl)
},
[isReadOnly]
[isReadonly]
)
if (storeWithStatus.error) {
@ -151,7 +155,7 @@ export function MultiplayerEditor({
assetUrls={assetUrls}
onMount={handleMount}
overrides={[sharingUiOverrides, fileSystemUiOverrides, cursorChatOverrides]}
initialState={isReadOnly ? 'hand' : 'select'}
initialState={isReadonly ? 'hand' : 'select'}
onUiEvent={handleUiEvent}
components={components}
autoFocus

Wyświetl plik

@ -1,4 +1,10 @@
import * as Popover from '@radix-ui/react-popover'
import {
GetReadonlySlugResponseBody,
ROOM_OPEN_MODE,
ROOM_PREFIX,
RoomOpenModeToPath,
} from '@tldraw/dotcom-shared'
import React, { useEffect, useState } from 'react'
import {
TldrawUiMenuContextProvider,
@ -15,29 +21,71 @@ import { createQRCodeImageDataString } from '../utils/qrcode'
import { SHARE_PROJECT_ACTION, SHARE_SNAPSHOT_ACTION } from '../utils/sharing'
import { ShareButton } from './ShareButton'
const SHARE_CURRENT_STATE = {
OFFLINE: 'offline',
SHARED_READ_WRITE: 'shared-read-write',
SHARED_READ_ONLY: 'shared-read-only',
} as const
type ShareCurrentState = (typeof SHARE_CURRENT_STATE)[keyof typeof SHARE_CURRENT_STATE]
type ShareState = {
state: 'offline' | 'shared' | 'readonly'
state: ShareCurrentState
qrCodeDataUrl: string
url: string
readonlyUrl: string
readonlyUrl: string | null
readonlyQrCodeDataUrl: string
}
function isSharedReadonlyUrl(pathname: string) {
return (
pathname.startsWith(`/${RoomOpenModeToPath[ROOM_OPEN_MODE.READ_ONLY]}/`) ||
pathname.startsWith(`/${RoomOpenModeToPath[ROOM_OPEN_MODE.READ_ONLY_LEGACY]}/`)
)
}
function isSharedReadWriteUrl(pathname: string) {
return pathname.startsWith(`/${ROOM_PREFIX}/`)
}
function getFreshShareState(): ShareState {
const isShared = window.location.href.includes('/r/')
const isReadOnly = window.location.href.includes('/v/')
const isSharedReadWrite = isSharedReadWriteUrl(window.location.pathname)
const isSharedReadOnly = isSharedReadonlyUrl(window.location.pathname)
return {
state: isShared ? 'shared' : isReadOnly ? 'readonly' : 'offline',
state: isSharedReadWrite
? SHARE_CURRENT_STATE.SHARED_READ_WRITE
: isSharedReadOnly
? SHARE_CURRENT_STATE.SHARED_READ_ONLY
: SHARE_CURRENT_STATE.OFFLINE,
url: window.location.href,
readonlyUrl: window.location.href.includes('/r/')
? getShareUrl(window.location.href, true)
: window.location.href,
readonlyUrl: isSharedReadOnly ? window.location.href : null,
qrCodeDataUrl: '',
readonlyQrCodeDataUrl: '',
}
}
async function getReadonlyUrl() {
const pathname = window.location.pathname
const isReadOnly = isSharedReadonlyUrl(pathname)
if (isReadOnly) return window.location.href
const segments = pathname.split('/')
const roomId = segments[2]
const result = await fetch(`/api/readonly-slug/${roomId}`)
if (!result.ok) return
const data = (await result.json()) as GetReadonlySlugResponseBody
if (!data.slug) return
segments[1] =
RoomOpenModeToPath[data.isLegacy ? ROOM_OPEN_MODE.READ_ONLY_LEGACY : ROOM_OPEN_MODE.READ_ONLY]
segments[2] = data.slug
const newPathname = segments.join('/')
return `${window.location.origin}${newPathname}${window.location.search}`
}
/** @public */
export const ShareMenu = React.memo(function ShareMenu() {
const msg = useTranslation()
@ -50,25 +98,24 @@ export const ShareMenu = React.memo(function ShareMenu() {
const [isUploading, setIsUploading] = useState(false)
const [isUploadingSnapshot, setIsUploadingSnapshot] = useState(false)
const [isReadOnlyLink, setIsReadOnlyLink] = useState(shareState.state === 'readonly')
const isReadOnlyLink = shareState.state === SHARE_CURRENT_STATE.SHARED_READ_ONLY
const currentShareLinkUrl = isReadOnlyLink ? shareState.readonlyUrl : shareState.url
const currentQrCodeUrl = isReadOnlyLink
? shareState.readonlyQrCodeDataUrl
: shareState.qrCodeDataUrl
const [didCopy, setDidCopy] = useState(false)
const [didCopyReadonlyLink, setDidCopyReadonlyLink] = useState(false)
const [didCopySnapshotLink, setDidCopySnapshotLink] = useState(false)
useEffect(() => {
if (shareState.state === 'offline') {
if (shareState.state === SHARE_CURRENT_STATE.OFFLINE) {
return
}
let cancelled = false
const shareUrl = getShareUrl(window.location.href, false)
const readonlyShareUrl = getShareUrl(window.location.href, true)
if (!shareState.qrCodeDataUrl && shareState.state === 'shared') {
if (!shareState.qrCodeDataUrl && shareState.state === SHARE_CURRENT_STATE.SHARED_READ_WRITE) {
// Fetch the QR code data URL
createQRCodeImageDataString(shareUrl).then((dataUrl) => {
if (!cancelled) {
@ -77,14 +124,16 @@ export const ShareMenu = React.memo(function ShareMenu() {
})
}
if (!shareState.readonlyQrCodeDataUrl) {
// fetch the readonly QR code data URL
createQRCodeImageDataString(readonlyShareUrl).then((dataUrl) => {
if (!cancelled) {
setShareState((s) => ({ ...s, readonlyShareUrl, readonlyQrCodeDataUrl: dataUrl }))
}
})
}
getReadonlyUrl().then((readonlyUrl) => {
if (readonlyUrl && !shareState.readonlyQrCodeDataUrl) {
// fetch the readonly QR code data URL
createQRCodeImageDataString(readonlyUrl).then((dataUrl) => {
if (!cancelled) {
setShareState((s) => ({ ...s, readonlyUrl, readonlyQrCodeDataUrl: dataUrl }))
}
})
}
})
const interval = setInterval(() => {
const url = window.location.href
@ -115,7 +164,8 @@ export const ShareMenu = React.memo(function ShareMenu() {
alignOffset={4}
>
<TldrawUiMenuContextProvider type="panel" sourceId="share-menu">
{shareState.state === 'shared' || shareState.state === 'readonly' ? (
{shareState.state === SHARE_CURRENT_STATE.SHARED_READ_WRITE ||
shareState.state === SHARE_CURRENT_STATE.SHARED_READ_ONLY ? (
<>
<button
className="tlui-share-zone__qr-code"
@ -124,41 +174,42 @@ export const ShareMenu = React.memo(function ShareMenu() {
isReadOnlyLink ? 'share-menu.copy-readonly-link' : 'share-menu.copy-link'
)}
onClick={() => {
if (!currentShareLinkUrl) return
setDidCopy(true)
setTimeout(() => setDidCopy(false), 1000)
navigator.clipboard.writeText(currentShareLinkUrl)
}}
/>
<TldrawUiMenuGroup id="copy">
<TldrawUiMenuItem
id="copy-to-clipboard"
readonlyOk
icon={didCopy ? 'clipboard-copied' : 'clipboard-copy'}
label={
isReadOnlyLink ? 'share-menu.copy-readonly-link' : 'share-menu.copy-link'
}
onSelect={() => {
setDidCopy(true)
setTimeout(() => setDidCopy(false), 750)
navigator.clipboard.writeText(currentShareLinkUrl)
}}
/>
{shareState.state === 'shared' && (
{shareState.state === SHARE_CURRENT_STATE.SHARED_READ_WRITE && (
<TldrawUiMenuItem
id="toggle-read-only"
label="share-menu.readonly-link"
icon={isReadOnlyLink ? 'check' : 'checkbox-empty'}
onSelect={async () => {
setIsReadOnlyLink(() => !isReadOnlyLink)
id="copy-to-clipboard"
readonlyOk
icon={didCopy ? 'clipboard-copied' : 'clipboard-copy'}
label="share-menu.copy-link"
onSelect={() => {
if (!shareState.url) return
setDidCopy(true)
setTimeout(() => setDidCopy(false), 750)
navigator.clipboard.writeText(shareState.url)
}}
/>
)}
<TldrawUiMenuItem
id="copy-readonly-to-clipboard"
readonlyOk
icon={didCopyReadonlyLink ? 'clipboard-copied' : 'clipboard-copy'}
label="share-menu.copy-readonly-link"
onSelect={() => {
if (!shareState.readonlyUrl) return
setDidCopyReadonlyLink(true)
setTimeout(() => setDidCopyReadonlyLink(false), 750)
navigator.clipboard.writeText(shareState.readonlyUrl)
}}
/>
<p className="tlui-menu__group tlui-share-zone__details">
{msg(
isReadOnlyLink
? 'share-menu.copy-readonly-link-note'
: 'share-menu.copy-link-note'
)}
{msg('share-menu.copy-readonly-link-note')}
</p>
</TldrawUiMenuGroup>
@ -185,6 +236,7 @@ export const ShareMenu = React.memo(function ShareMenu() {
<TldrawUiMenuGroup id="share">
<TldrawUiMenuItem
id="share-project"
readonlyOk
label="share-menu.share-project"
icon="share-1"
onSelect={async () => {
@ -197,7 +249,7 @@ export const ShareMenu = React.memo(function ShareMenu() {
/>
<p className="tlui-menu__group tlui-share-zone__details">
{msg(
shareState.state === 'offline'
shareState.state === SHARE_CURRENT_STATE.OFFLINE
? 'share-menu.offline-note'
: isReadOnlyLink
? 'share-menu.copy-readonly-link-note'
@ -208,6 +260,7 @@ export const ShareMenu = React.memo(function ShareMenu() {
<TldrawUiMenuGroup id="copy-snapshot-link">
<TldrawUiMenuItem
id="copy-snapshot-link"
readonlyOk
icon={didCopySnapshotLink ? 'clipboard-copied' : 'clipboard-copy'}
label={unwrapLabel(shareSnapshot.label)}
onSelect={async () => {

Wyświetl plik

@ -1,9 +1,11 @@
import { TLIncompatibilityReason } from '@tldraw/tlsync'
import { ErrorScreen, exhaustiveSwitchError } from 'tldraw'
import { exhaustiveSwitchError } from 'tldraw'
import { RemoteSyncError } from '../utils/remote-sync/remote-sync'
import { ErrorPage } from './ErrorPage/ErrorPage'
export function StoreErrorScreen({ error }: { error: Error }) {
let message = 'Could not connect to server.'
let header = 'Could not connect to server.'
let message = ''
if (error instanceof RemoteSyncError) {
switch (error.reason) {
@ -26,14 +28,15 @@ export function StoreErrorScreen({ error }: { error: Error }) {
'Your changes were rejected by the server. Please reload the page. If the problem persists contact the system administrator.'
break
}
case TLIncompatibilityReason.RoomNotFound: {
header = 'Room not found'
message = 'The room you are trying to connect to does not exist.'
break
}
default:
exhaustiveSwitchError(error.reason)
}
}
return (
<div className="tldraw__editor tl-container">
<ErrorScreen>{message}</ErrorScreen>
</div>
)
return <ErrorPage icon messages={{ header, para1: message }} />
}

Wyświetl plik

@ -1,4 +1,10 @@
import { TLSyncClient, schema } from '@tldraw/tlsync'
import {
TLCloseEventCode,
TLIncompatibilityReason,
TLPersistentClientSocketStatus,
TLSyncClient,
schema,
} from '@tldraw/tlsync'
import { useEffect, useState } from 'react'
import {
TAB_ID,
@ -55,6 +61,16 @@ export function useRemoteSyncClient(opts: UseSyncClientConfig): RemoteTLStoreWit
return withParams.toString()
})
socket.onStatusChange((val: TLPersistentClientSocketStatus, closeCode?: number) => {
if (val === 'error' && closeCode === TLCloseEventCode.NOT_FOUND) {
trackAnalyticsEvent(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })
setState({ error: new RemoteSyncError(TLIncompatibilityReason.RoomNotFound) })
client.close()
socket.close()
return
}
})
let didCancel = false
const client = new TLSyncClient({

Wyświetl plik

@ -1,8 +1,9 @@
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
import { RoomSnapshot } from '@tldraw/tlsync'
import '../../styles/globals.css'
import { BoardHistorySnapshot } from '../components/BoardHistorySnapshot/BoardHistorySnapshot'
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { defineLoader } from '../utils/defineLoader'
const { loader, useData } = defineLoader(async (args) => {
@ -11,7 +12,7 @@ const { loader, useData } = defineLoader(async (args) => {
if (!roomId) return null
const result = await fetch(`/api/r/${roomId}/history/${timestamp}`, {
const result = await fetch(`/api/${ROOM_PREFIX}/${roomId}/history/${timestamp}`, {
headers: {},
})
if (!result.ok) return null
@ -32,13 +33,12 @@ export function Component() {
header: 'Page not found',
para1: 'The page you are looking does not exist or has been moved.',
}}
redirectTo="/"
/>
)
const { data, roomId, timestamp } = result
return (
<IFrameProtector slug={roomId} context="history-snapshot">
<IFrameProtector slug={roomId} context={ROOM_CONTEXT.HISTORY_SNAPSHOT}>
<BoardHistorySnapshot data={data} roomId={roomId} timestamp={timestamp} />
</IFrameProtector>
)

Wyświetl plik

@ -1,6 +1,7 @@
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
import { BoardHistoryLog } from '../components/BoardHistoryLog/BoardHistoryLog'
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { defineLoader } from '../utils/defineLoader'
const { loader, useData } = defineLoader(async (args) => {
@ -8,7 +9,7 @@ const { loader, useData } = defineLoader(async (args) => {
if (!boardId) return null
const result = await fetch(`/api/r/${boardId}/history`, {
const result = await fetch(`/api/${ROOM_PREFIX}/${boardId}/history`, {
headers: {},
})
if (!result.ok) return null
@ -29,11 +30,10 @@ export function Component() {
header: 'Page not found',
para1: 'The page you are looking does not exist or has been moved.',
}}
redirectTo="/"
/>
)
return (
<IFrameProtector slug={data.boardId} context="history">
<IFrameProtector slug={data.boardId} context={ROOM_CONTEXT.HISTORY}>
<BoardHistoryLog data={data.data} />
</IFrameProtector>
)

Wyświetl plik

@ -0,0 +1,40 @@
import { ROOM_PREFIX, Snapshot } from '@tldraw/dotcom-shared'
import { schema } from '@tldraw/tlsync'
import { Navigate } from 'react-router-dom'
import '../../styles/globals.css'
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
import { defineLoader } from '../utils/defineLoader'
import { isInIframe } from '../utils/iFrame'
import { getNewRoomResponse } from '../utils/sharing'
const { loader, useData } = defineLoader(async (_args) => {
if (isInIframe()) return null
const res = await getNewRoomResponse({
schema: schema.serialize(),
snapshot: {},
} satisfies Snapshot)
const response = (await res.json()) as { error: boolean; slug?: string }
if (!res.ok || response.error || !response.slug) {
return null
}
return { slug: response.slug }
})
export { loader }
export function Component() {
const data = useData()
if (!data)
return (
<ErrorPage
icon
messages={{
header: 'Page not found',
para1: 'The page you are looking does not exist or has been moved.',
}}
/>
)
return <Navigate to={`/${ROOM_PREFIX}/${data.slug}`} />
}

Wyświetl plik

@ -8,7 +8,6 @@ export function Component() {
header: 'Page not found',
para1: 'The page you are looking does not exist or has been moved.',
}}
redirectTo="/"
/>
)
}

Wyświetl plik

@ -1,13 +1,14 @@
import { ROOM_OPEN_MODE } from '@tldraw/dotcom-shared'
import { useParams } from 'react-router-dom'
import '../../styles/globals.css'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { MultiplayerEditor } from '../components/MultiplayerEditor'
export function Component() {
const id = useParams()['roomId'] as string
return (
<IFrameProtector slug={id} context="public-multiplayer">
<MultiplayerEditor isReadOnly={false} roomSlug={id} />
<IFrameProtector slug={id} context={ROOM_CONTEXT.PUBLIC_MULTIPLAYER}>
<MultiplayerEditor roomOpenMode={ROOM_OPEN_MODE.READ_WRITE} roomSlug={id} />
</IFrameProtector>
)
}

Wyświetl plik

@ -0,0 +1,14 @@
import { ROOM_OPEN_MODE } from '@tldraw/dotcom-shared'
import { useParams } from 'react-router-dom'
import '../../styles/globals.css'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { MultiplayerEditor } from '../components/MultiplayerEditor'
export function Component() {
const id = useParams()['roomId'] as string
return (
<IFrameProtector slug={id} context={ROOM_CONTEXT.PUBLIC_READONLY}>
<MultiplayerEditor roomOpenMode={ROOM_OPEN_MODE.READ_ONLY_LEGACY} roomSlug={id} />
</IFrameProtector>
)
}

Wyświetl plik

@ -1,13 +1,14 @@
import { ROOM_OPEN_MODE } from '@tldraw/dotcom-shared'
import { useParams } from 'react-router-dom'
import '../../styles/globals.css'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { MultiplayerEditor } from '../components/MultiplayerEditor'
export function Component() {
const id = useParams()['roomId'] as string
return (
<IFrameProtector slug={id} context="public-readonly">
<MultiplayerEditor isReadOnly={true} roomSlug={id} />
<IFrameProtector slug={id} context={ROOM_CONTEXT.PUBLIC_READONLY}>
<MultiplayerEditor roomOpenMode={ROOM_OPEN_MODE.READ_ONLY} roomSlug={id} />
</IFrameProtector>
)
}

Wyświetl plik

@ -1,6 +1,6 @@
import { SerializedSchema, TLRecord } from 'tldraw'
import '../../styles/globals.css'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { SnapshotsEditor } from '../components/SnapshotsEditor'
import { defineLoader } from '../utils/defineLoader'
@ -23,7 +23,7 @@ export function Component() {
if (!result) throw Error('Room not found')
const { roomId, records, schema } = result
return (
<IFrameProtector slug={roomId} context="public-snapshot">
<IFrameProtector slug={roomId} context={ROOM_CONTEXT.PUBLIC_SNAPSHOT}>
<SnapshotsEditor records={records} schema={schema} />
</IFrameProtector>
)

Wyświetl plik

@ -1,10 +1,10 @@
import '../../styles/globals.css'
import { IFrameProtector } from '../components/IFrameProtector'
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
import { LocalEditor } from '../components/LocalEditor'
export function Component() {
return (
<IFrameProtector slug="home" context="local">
<IFrameProtector slug="home" context={ROOM_CONTEXT.LOCAL}>
<LocalEditor />
</IFrameProtector>
)

Wyświetl plik

@ -1,7 +1,12 @@
import { captureException } from '@sentry/react'
import { nanoid } from 'nanoid'
import {
READ_ONLY_LEGACY_PREFIX,
READ_ONLY_PREFIX,
ROOM_PREFIX,
SNAPSHOT_PREFIX,
} from '@tldraw/dotcom-shared'
import { useEffect } from 'react'
import { createRoutesFromElements, Outlet, redirect, Route, useRouteError } from 'react-router-dom'
import { Outlet, Route, createRoutesFromElements, useRouteError } from 'react-router-dom'
import { DefaultErrorFallback } from './components/DefaultErrorFallback/DefaultErrorFallback'
import { ErrorPage } from './components/ErrorPage/ErrorPage'
@ -30,28 +35,20 @@ export const router = createRoutesFromElements(
>
<Route errorElement={<DefaultErrorFallback />}>
<Route path="/" lazy={() => import('./pages/root')} />
<Route path={`/${ROOM_PREFIX}`} lazy={() => import('./pages/new')} />
<Route path="/new" lazy={() => import('./pages/new')} />
<Route path={`/${ROOM_PREFIX}/:roomId`} lazy={() => import('./pages/public-multiplayer')} />
<Route path={`/${ROOM_PREFIX}/:boardId/history`} lazy={() => import('./pages/history')} />
<Route
path="/r"
loader={() => {
const id = 'v2' + nanoid()
return redirect(`/r/${id}`)
}}
/>
<Route
path="/new"
loader={() => {
const id = 'v2' + nanoid()
return redirect(`/r/${id}`)
}}
/>
<Route path="/r/:roomId" lazy={() => import('./pages/public-multiplayer')} />
<Route path="/r/:boardId/history" lazy={() => import('./pages/history')} />
<Route
path="/r/:boardId/history/:timestamp"
path={`/${ROOM_PREFIX}/:boardId/history/:timestamp`}
lazy={() => import('./pages/history-snapshot')}
/>
<Route path="/s/:roomId" lazy={() => import('./pages/public-snapshot')} />
<Route path="/v/:roomId" lazy={() => import('./pages/public-readonly')} />
<Route path={`/${SNAPSHOT_PREFIX}/:roomId`} lazy={() => import('./pages/public-snapshot')} />
<Route
path={`/${READ_ONLY_LEGACY_PREFIX}/:roomId`}
lazy={() => import('./pages/public-readonly-legacy')}
/>
<Route path={`/${READ_ONLY_PREFIX}/:roomId`} lazy={() => import('./pages/public-readonly')} />
</Route>
<Route path="*" lazy={() => import('./pages/not-found')} />
</Route>

Wyświetl plik

@ -1,4 +1,3 @@
// eslint-disable-next-line import/no-internal-modules
import { getAssetUrlsByImport } from '@tldraw/assets/imports.vite'
export const assetUrls = getAssetUrlsByImport()

Wyświetl plik

@ -0,0 +1,16 @@
export const isInIframe = () => {
return typeof window !== 'undefined' && (window !== window.top || window.self !== window.parent)
}
export function getParentOrigin() {
if (isInIframe()) {
const ancestorOrigins = window.location.ancestorOrigins
// ancestorOrigins is not supported in Firefox
if (ancestorOrigins && ancestorOrigins.length > 0) {
return ancestorOrigins[0]
} else {
return document.referrer
}
}
return document.location.origin
}

Wyświetl plik

@ -155,20 +155,33 @@ describe(ClientWebSocketAdapter, () => {
it('signals status changes', async () => {
const onStatusChange = jest.fn()
adapter.onStatusChange(onStatusChange)
await waitFor(() => adapter._ws?.readyState === WebSocket.OPEN)
expect(onStatusChange).toHaveBeenCalledWith('online')
connectedServerSocket.terminate()
await waitFor(() => adapter._ws?.readyState === WebSocket.CLOSED)
expect(onStatusChange).toHaveBeenCalledWith('offline')
expect(onStatusChange).toHaveBeenCalledWith('offline', 1006)
await waitFor(() => adapter._ws?.readyState === WebSocket.OPEN)
expect(onStatusChange).toHaveBeenCalledWith('online')
connectedServerSocket.terminate()
await waitFor(() => adapter._ws?.readyState === WebSocket.CLOSED)
expect(onStatusChange).toHaveBeenCalledWith('offline')
expect(onStatusChange).toHaveBeenCalledWith('offline', 1006)
await waitFor(() => adapter._ws?.readyState === WebSocket.OPEN)
expect(onStatusChange).toHaveBeenCalledWith('online')
adapter._ws?.onerror?.({} as any)
expect(onStatusChange).toHaveBeenCalledWith('error')
expect(onStatusChange).toHaveBeenCalledWith('error', undefined)
})
it('signals the correct closeCode when a room is not found', async () => {
const onStatusChange = jest.fn()
adapter.onStatusChange(onStatusChange)
await waitFor(() => adapter._ws?.readyState === WebSocket.OPEN)
adapter._ws!.onclose?.({ code: 4099 } as any)
expect(onStatusChange).toHaveBeenCalledWith('error', 4099)
})
it('signals status changes while restarting', async () => {
@ -181,7 +194,7 @@ describe(ClientWebSocketAdapter, () => {
await waitFor(() => onStatusChange.mock.calls.length === 2)
expect(onStatusChange).toHaveBeenCalledWith('offline')
expect(onStatusChange).toHaveBeenCalledWith('offline', undefined)
expect(onStatusChange).toHaveBeenCalledWith('online')
})
})

Wyświetl plik

@ -1,5 +1,6 @@
import {
chunk,
TLCloseEventCode,
TLPersistentClientSocket,
TLPersistentClientSocketStatus,
TLSocketClientSentEvent,
@ -68,15 +69,20 @@ export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord
this._reconnectManager.connected()
}
private _handleDisconnect(reason: 'closed' | 'error' | 'manual') {
private _handleDisconnect(reason: 'closed' | 'error' | 'manual', closeCode?: number) {
debug('handleDisconnect', {
currentStatus: this.connectionStatus,
closeCode,
reason,
})
let newStatus: 'offline' | 'error'
switch (reason) {
case 'closed':
if (closeCode === TLCloseEventCode.NOT_FOUND) {
newStatus = 'error'
break
}
newStatus = 'offline'
break
case 'error':
@ -94,7 +100,7 @@ export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord
!(newStatus === 'error' && this.connectionStatus === 'offline')
) {
this._connectionStatus.set(newStatus)
this.statusListeners.forEach((cb) => cb(newStatus))
this.statusListeners.forEach((cb) => cb(newStatus, closeCode))
}
this._reconnectManager.disconnected()
@ -120,10 +126,10 @@ export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord
)
this._handleConnect()
}
ws.onclose = () => {
ws.onclose = (event: CloseEvent) => {
debug('ws.onclose')
if (this._ws === ws) {
this._handleDisconnect('closed')
this._handleDisconnect('closed', event.code)
} else {
debug('ignoring onclose for an orphaned socket')
}
@ -194,8 +200,10 @@ export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord
}
}
private statusListeners = new Set<(status: TLPersistentClientSocketStatus) => void>()
onStatusChange(cb: (val: TLPersistentClientSocketStatus) => void) {
private statusListeners = new Set<
(status: TLPersistentClientSocketStatus, closeCode?: number) => void
>()
onStatusChange(cb: (val: TLPersistentClientSocketStatus, closeCode?: number) => void) {
assert(!this.isDisposed, 'Tried to add status listener on a disposed socket')
this.statusListeners.add(cb)

Wyświetl plik

@ -1,10 +1,16 @@
import {
CreateRoomRequestBody,
CreateSnapshotRequestBody,
CreateSnapshotResponseBody,
ROOM_PREFIX,
SNAPSHOT_PREFIX,
Snapshot,
} from '@tldraw/dotcom-shared'
import { useMemo } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import {
AssetRecordType,
Editor,
SerializedSchema,
SerializedStore,
TLAsset,
TLAssetId,
TLRecord,
@ -20,6 +26,7 @@ import { useMultiplayerAssets } from '../hooks/useMultiplayerAssets'
import { getViewportUrlQuery } from '../hooks/useUrlState'
import { cloneAssetForShare } from './cloneAssetForShare'
import { ASSET_UPLOADER_URL } from './config'
import { getParentOrigin, isInIframe } from './iFrame'
import { shouldLeaveSharedProject } from './shouldLeaveSharedProject'
import { trackAnalyticsEvent } from './trackAnalyticsEvent'
import { UI_OVERRIDE_TODO_EVENT, useHandleUiEvents } from './useHandleUiEvent'
@ -32,27 +39,6 @@ export const FORK_PROJECT_ACTION = 'fork-project' as const
const CREATE_SNAPSHOT_ENDPOINT = `/api/snapshots`
const SNAPSHOT_UPLOAD_URL = `/api/new-room`
type SnapshotRequestBody = {
schema: SerializedSchema
snapshot: SerializedStore<TLRecord>
}
type CreateSnapshotRequestBody = {
schema: SerializedSchema
snapshot: SerializedStore<TLRecord>
parent_slug?: string | string[] | undefined
}
type CreateSnapshotResponseBody =
| {
error: false
roomId: string
}
| {
error: true
message: string
}
async function getSnapshotLink(
source: string,
editor: Editor,
@ -85,16 +71,30 @@ async function getSnapshotLink(
}
const paramsToUse = getViewportUrlQuery(editor)
const params = paramsToUse ? `?${new URLSearchParams(paramsToUse).toString()}` : ''
return new Blob([`${window.location.origin}/s/${response.roomId}${params}`], {
return new Blob([`${window.location.origin}/${SNAPSHOT_PREFIX}/${response.roomId}${params}`], {
type: 'text/plain',
})
}
export async function getNewRoomResponse(snapshot: Snapshot) {
return await fetch(SNAPSHOT_UPLOAD_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
origin: getParentOrigin(),
snapshot,
} satisfies CreateRoomRequestBody),
})
}
export function useSharing(): TLUiOverrides {
const navigate = useNavigate()
const id = useSearchParams()[0].get('id') ?? undefined
const uploadFileToAsset = useMultiplayerAssets(ASSET_UPLOADER_URL)
const handleUiEvent = useHandleUiEvents()
const runningInIFrame = isInIframe()
return useMemo(
(): TLUiOverrides => ({
@ -122,17 +122,10 @@ export function useSharing(): TLUiOverrides {
const data = await getRoomData(editor, addToast, msg, uploadFileToAsset)
if (!data) return
const res = await fetch(SNAPSHOT_UPLOAD_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
schema: editor.store.schema.serialize(),
snapshot: data,
} satisfies SnapshotRequestBody),
const res = await getNewRoomResponse({
schema: editor.store.schema.serialize(),
snapshot: data,
})
const response = (await res.json()) as { error: boolean; slug?: string }
if (!res.ok || response.error) {
console.error(await res.text())
@ -140,8 +133,13 @@ export function useSharing(): TLUiOverrides {
}
const query = getViewportUrlQuery(editor)
navigate(`/r/${response.slug}?${new URLSearchParams(query ?? {}).toString()}`)
const origin = window.location.origin
const pathname = `/${ROOM_PREFIX}/${response.slug}?${new URLSearchParams(query ?? {}).toString()}`
if (runningInIFrame) {
window.open(`${origin}${pathname}`)
} else {
navigate(pathname)
}
} catch (error) {
console.error(error)
addToast({
@ -182,12 +180,12 @@ export function useSharing(): TLUiOverrides {
actions[FORK_PROJECT_ACTION] = {
...actions[SHARE_PROJECT_ACTION],
id: FORK_PROJECT_ACTION,
label: 'action.fork-project',
label: runningInIFrame ? 'action.fork-project-on-tldraw' : 'action.fork-project',
}
return actions
},
}),
[handleUiEvent, navigate, uploadFileToAsset, id]
[handleUiEvent, navigate, uploadFileToAsset, id, runningInIFrame]
)
}

Wyświetl plik

@ -183,6 +183,7 @@ a {
font-weight: 500;
color: var(--text-color-2);
padding: 12px 4px;
text-decoration: underline;
}
/* ------------------ Board history ----------------- */

Wyświetl plik

@ -28,6 +28,9 @@
{
"path": "../../packages/assets"
},
{
"path": "../../packages/dotcom-shared"
},
{
"path": "../../packages/tldraw"
},

Wyświetl plik

@ -28,6 +28,10 @@ export function ExamplePage({
children: React.ReactNode
}) {
const categories = examples.map((e) => e.id)
const [filterValue, setFilterValue] = useState('')
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFilterValue(e.target.value)
}
return (
<DialogContextProvider>
@ -69,6 +73,12 @@ export function ExamplePage({
Develop
</a>
</div>
<input
className="example__sidebar__filter"
placeholder="Filter…"
value={filterValue}
onChange={handleFilterChange}
/>
<ul className="example__sidebar__categories scroll-light">
{categories.map((currentCategory) => (
<li key={currentCategory} className="example__sidebar__category">
@ -76,7 +86,10 @@ export function ExamplePage({
<ul className="example__sidebar__category__items">
{examples
.find((category) => category.id === currentCategory)
?.value.map((sidebarExample) => (
?.value.filter((example) =>
example.title.toLowerCase().includes(filterValue.toLowerCase())
)
.map((sidebarExample) => (
<ExampleSidebarListItem
key={sidebarExample.path}
example={sidebarExample}

Wyświetl plik

@ -77,9 +77,7 @@ const ContextToolbarComponent = track(() => {
width: 32,
background: isActive ? 'var(--color-muted-2)' : 'transparent',
}}
onClick={() =>
editor.setStyleForSelectedShapes(DefaultSizeStyle, value, { squashing: false })
}
onClick={() => editor.setStyleForSelectedShapes(DefaultSizeStyle, value)}
>
<TldrawUiIcon icon={icon} />
</div>

Wyświetl plik

@ -1,5 +1,4 @@
import { createShapePropsMigrationIds } from '@tldraw/tlschema/src/records/TLShape'
import { createShapePropsMigrationSequence } from 'tldraw'
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from 'tldraw'
const versions = createShapePropsMigrationIds(
// this must match the shape type in the shape definition

Wyświetl plik

@ -25,7 +25,7 @@ function CustomStylePanel(props: TLUiStylePanelProps) {
<TldrawUiButton
type="menu"
onClick={() => {
editor.setStyleForSelectedShapes(DefaultColorStyle, 'red', { squashing: true })
editor.setStyleForSelectedShapes(DefaultColorStyle, 'red')
}}
>
<TldrawUiButtonLabel>Red</TldrawUiButtonLabel>
@ -35,7 +35,7 @@ function CustomStylePanel(props: TLUiStylePanelProps) {
<TldrawUiButton
type="menu"
onClick={() => {
editor.setStyleForSelectedShapes(DefaultColorStyle, 'green', { squashing: true })
editor.setStyleForSelectedShapes(DefaultColorStyle, 'green')
}}
>
<TldrawUiButtonLabel>Green</TldrawUiButtonLabel>

Wyświetl plik

@ -9,12 +9,12 @@ import {
TldrawSelectionBackground,
TldrawSelectionForeground,
TldrawUi,
defaultEditorAssetUrls,
defaultShapeTools,
defaultShapeUtils,
defaultTools,
usePreloadAssets,
} from 'tldraw'
import { defaultEditorAssetUrls } from 'tldraw/src/lib/utils/static-assets/assetUrls'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this file!

Wyświetl plik

@ -3,6 +3,7 @@ import {
AssetRecordType,
Box,
Editor,
PORTRAIT_BREAKPOINT,
SVGContainer,
TLImageShape,
TLShapeId,
@ -16,7 +17,6 @@ import {
useBreakpoint,
useEditor,
} from 'tldraw'
import { PORTRAIT_BREAKPOINT } from 'tldraw/src/lib/ui/constants'
import { AnnotatorImage } from './types'
// TODO:

Wyświetl plik

@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo } from 'react'
import {
Box,
PORTRAIT_BREAKPOINT,
SVGContainer,
TLImageShape,
TLShapeId,
@ -15,7 +16,6 @@ import {
useBreakpoint,
useEditor,
} from 'tldraw'
import { PORTRAIT_BREAKPOINT } from 'tldraw/src/lib/ui/constants'
import { ExportPdfButton } from './ExportPdfButton'
import { Pdf } from './PdfPicker'

Wyświetl plik

@ -6,6 +6,8 @@ import {
TLOnResizeHandler,
TLStoreSnapshot,
Tldraw,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
resizeBox,
} from 'tldraw'
import 'tldraw/tldraw.css'
@ -22,6 +24,31 @@ export type IMyShape = TLBaseShape<
}
>
// [1]
const versions = createShapePropsMigrationIds(
// this must match the shape type in the shape definition
'myshape',
{
AddColor: 1,
}
)
// [2]
export const cardShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: versions.AddColor,
up(props) {
// it is safe to mutate the props object here
props.color = 'lightblue'
},
down(props) {
delete props.color
},
},
],
})
export class MigratedShapeUtil extends BaseBoxShapeUtil<IMyShape> {
static override type = 'myshape' as const
@ -31,31 +58,8 @@ export class MigratedShapeUtil extends BaseBoxShapeUtil<IMyShape> {
color: T.string,
}
// [1]
static override migrations = {
firstVersion: 0,
currentVersion: 1,
migrators: {
1: {
up(shape: IMyShape) {
return {
...shape,
props: {
...shape.props,
color: 'lightblue',
},
}
},
down(shape: IMyShape) {
const { color: _, ...propsWithoutColor } = shape.props
return {
...shape,
props: propsWithoutColor,
}
},
},
},
}
// [3]
static override migrations = cardShapeMigrations
getDefaultProps(): IMyShape['props'] {
return {
@ -104,59 +108,56 @@ export default function ShapeWithMigrationsExample() {
/*
Introduction:
Sometimes you'll want to update the way a shape works in your application without
breaking older versions of the shape that a user may have stored or persisted in
memory.
Sometimes you'll want to update the way a shape works in your application without breaking older
versions of the shape that a user may have stored or persisted in memory.
This example shows how you can use our migrations system to upgrade (or downgrade)
user's data between different versions. Most of the code above is general "custom
shape" codesee our custom shape example for more details.
This example shows how you can use our migrations system to upgrade (or downgrade) user's data
between different versions. Most of the code above is general "custom shape" codesee our custom
shape example for more details.
[1]
To define migrations, we can override the migrations property of our shape util. Each migration
had two parts: an `up` migration and `down` migration. In this case, the `up` migration adds
the `color` prop to the shape, and the `down` migration removes it.
[1] First, we need IDs for each migration. List each change with its version number. Once you've
added a migration, it should not change again.
In some cases (mainly in multiplayer sessions) a peer or server may need to take a later
version of a shape and migrate it down to an older versionin this case, it would run the
down migrations in order to get it to the needed version.
[2] Next, we create a migration sequence. This is where we actually write our migration logic. Each
migration had three parts: an `id` (created in [1]), an `up` migration and `down` migration. In this
case, the `up` migration adds the `color` prop to the shape, and the `down` migration removes it.
In some cases (mainly in multiplayer sessions) a peer or server may need to take a later version of
a shape and migrate it down to an older versionin this case, it would run the down migrations in
order to get it to the needed version.
[3] Finally, we add our migrations to the ShapeUtil. This tells tldraw about the migrations so they
can be used with your shapes.
How it works:
Each time the editor's store creates a snapshot (`editor.store.createSnapshot`), it
serializes all of the records (the snapshot's `store`) as well as versions of each
record that it contains (the snapshot's `scena`). When the editor loads a snapshot,
it compares its current schema with the snapshot's schema to determine which migrations
to apply to each record.
Each time the editor's store creates a snapshot (`editor.store.createSnapshot`), it serializes all
of the records (the snapshot's `store`) as well as versions of each record that it contains (the
snapshot's `scena`). When the editor loads a snapshot, it compares its current schema with the
snapshot's schema to determine which migrations to apply to each record.
In this example, we have a snapshot (snapshot.json) that we created in version 0,
however our shape now has a 'color' prop that was added in version 1.
In this example, we have a snapshot (snapshot.json) that we created in version 0, however our shape
now has a 'color' prop that was added in version 1.
The snapshot looks something like this:
```json{
{
"store": {
"shape:BqG5uIAa9ig2-ukfnxwBX": {
"store": {
"shape:BqG5uIAa9ig2-ukfnxwBX": {
...,
"props": {
"w": 300,
"h": 300
},
},
},
"schema": {
...,
"sequences": {
...,
"props": {
"w": 300,
"h": 300
},
},
"schema": {
...,
"recordVersions": {
...,
"shape": {
"version": 3,
"subTypeKey": "type",
"subTypeVersions": {
...,
"myshape": 0
}
}
}
"com.tldraw.shape.arrow": 4,
"com.tldraw.shape.myshape": 0
}
}
}
@ -166,9 +167,8 @@ Note that the shape in the snapshot doesn't have a 'color' prop.
Note also that the schema's version for this shape is 0.
When the editor loads the snapshot, it will compare the serialzied schema's version with
its current schema's version for the shape, which is 1 as defined in our shape's migrations.
Since the serialized version is older than its current version, it will use our migration
to bring it up to date: it will run the migration's `up` function, which will add the 'color'
prop to the shape.
When the editor loads the snapshot, it will compare the serialzied schema's version with its current
schema's version for the shape, which is 1 as defined in our shape's migrations. Since the
serialized version is older than its current version, it will use our migration to bring it up to
date: it will run the migration's `up` function, which will add the 'color' prop to the shape.
*/

Wyświetl plik

@ -8,8 +8,8 @@ import {
TLDefaultColorStyle,
TLDefaultSizeStyle,
Tldraw,
useDefaultColorTheme,
} from 'tldraw'
import { useDefaultColorTheme } from 'tldraw/src/lib/shapes/shared/ShapeFill'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this file!

Wyświetl plik

@ -8,10 +8,10 @@ import {
T,
TLBaseShape,
TLOnResizeHandler,
getPerfectDashProps,
resizeBox,
useValue,
} from 'tldraw'
import { getPerfectDashProps } from 'tldraw/src/lib/shapes/shared/getPerfectDashProps'
import { moveToSlide, useSlides } from './useSlides'
export type SlideShape = TLBaseShape<

Wyświetl plik

@ -22,9 +22,9 @@ import {
ZERO_INDEX_KEY,
resizeBox,
structuredClone,
useDefaultColorTheme,
vecModelValidator,
} from 'tldraw'
import { useDefaultColorTheme } from 'tldraw/src/lib/shapes/shared/ShapeFill'
import { getSpeechBubbleVertices, getTailIntersectionPoint } from './helpers'
// Copied from tldraw/tldraw

Wyświetl plik

@ -29,7 +29,9 @@ export default function UserPresenceExample() {
chatMessage: CURSOR_CHAT_MESSAGE,
})
editor.store.put([peerPresence])
editor.store.mergeRemoteChanges(() => {
editor.store.put([peerPresence])
})
// [b]
const raf = rRaf.current
@ -67,23 +69,29 @@ export default function UserPresenceExample() {
)
}
editor.store.put([
{
...peerPresence,
cursor,
chatMessage,
lastActivityTimestamp: now,
},
])
editor.store.mergeRemoteChanges(() => {
editor.store.put([
{
...peerPresence,
cursor,
chatMessage,
lastActivityTimestamp: now,
},
])
})
rRaf.current = requestAnimationFrame(loop)
}
rRaf.current = requestAnimationFrame(loop)
} else {
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
rRaf.current = setInterval(() => {
editor.store.mergeRemoteChanges(() => {
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
})
rRaf.current = setInterval(() => {
editor.store.mergeRemoteChanges(() => {
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
})
}, 1000)
}
}}

Wyświetl plik

@ -1,4 +1,4 @@
import { TLExportType } from 'tldraw/src/lib/utils/export/exportAs'
import { TLExportType } from 'tldraw'
export interface EndToEndApi {
exportAsSvg: () => void

Wyświetl plik

@ -70,7 +70,7 @@ html,
width: 256px;
min-width: 256px;
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-rows: auto 48px 48px 1fr auto;
border-right: 1px solid var(--black-transparent-light);
overflow: hidden;
font-size: 14px;
@ -83,6 +83,14 @@ html,
color: inherit;
}
.example__sidebar__filter {
margin: 8px;
padding: 8px;
border-radius: 6px;
border: 1px solid #e8e8e8;
font-size: 14px;
}
/* Header */
.example__sidebar__header {
@ -131,10 +139,15 @@ ul.example__sidebar__categories {
/* Category */
li.example__sidebar__category {
display: none;
position: relative;
padding: 8px 0px;
}
.example__sidebar__category:has(> ul > li) {
display: block;
}
ul.example__sidebar__category__items {
list-style: none;
padding: 0px 0px 0px 4px;

Wyświetl plik

@ -57,11 +57,11 @@ export const ChangeResponder = () => {
type: 'vscode:editor-loaded',
})
editor.on('change-history', handleChange)
const dispose = editor.store.listen(handleChange, { scope: 'document' })
return () => {
handleChange()
editor.off('change-history', handleChange)
dispose()
}
}, [editor])

Wyświetl plik

@ -1,6 +1,3 @@
// eslint-disable-next-line import/no-internal-modules
import 'tldraw/tldraw.css'
// eslint-disable-next-line import/no-internal-modules
import { getAssetUrlsByImport } from '@tldraw/assets/imports'
import { useCallback, useEffect, useMemo, useState } from 'react'
import {
@ -13,6 +10,7 @@ import {
TldrawUiMenuGroup,
setRuntimeOverrides,
} from 'tldraw'
import 'tldraw/tldraw.css'
import { VscodeMessage } from '../../messages'
import '../public/index.css'
import { ChangeResponder } from './ChangeResponder'

Wyświetl plik

@ -1,4 +1,4 @@
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 2V4H18V2H8ZM6 1.5C6 0.671573 6.67157 0 7.5 0H18.5C19.3284 0 20 0.671572 20 1.5V2H21C22.6569 2 24 3.34315 24 5V14H22V5C22 4.44772 21.5523 4 21 4H20V4.5C20 5.32843 19.3284 6 18.5 6H7.5C6.67157 6 6 5.32843 6 4.5V4H5C4.44771 4 4 4.44772 4 5V25C4 25.5523 4.44772 26 5 26H12V28H5C3.34315 28 2 26.6569 2 25V5C2 3.34314 3.34315 2 5 2H6V1.5Z" fill="black"/>
<path d="M27.5197 17.173C28.0099 17.4936 28.1475 18.1509 27.827 18.6411L20.6149 29.6713C20.445 29.9313 20.1696 30.1037 19.8615 30.143C19.5534 30.1823 19.2436 30.0846 19.0138 29.8757L14.3472 25.6333C13.9137 25.2393 13.8818 24.5685 14.2758 24.1351C14.6698 23.7017 15.3406 23.6697 15.774 24.0638L19.5203 27.4694L26.0516 17.4803C26.3721 16.9901 27.0294 16.8525 27.5197 17.173Z" fill="black"/>
</svg>

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 893 B

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 893 B

Wyświetl plik

@ -48,6 +48,7 @@
"action.flip-horizontal.short": "Flip H",
"action.flip-vertical.short": "Flip V",
"action.fork-project": "Fork this project",
"action.fork-project-on-tldraw": "Fork project on tldraw",
"action.group": "Group",
"action.insert-embed": "Insert embed",
"action.insert-media": "Upload media",
@ -262,7 +263,7 @@
"share-menu.copy-readonly-link": "Copy read-only link",
"share-menu.offline-note": "Create a new shared project based on your current project.",
"share-menu.copy-link-note": "Anyone with the link will be able to view and edit this project.",
"share-menu.copy-readonly-link-note": "Anyone with the link will be able to view (but not edit) this project.",
"share-menu.copy-readonly-link-note": "Anyone with the link will be able to access this project.",
"share-menu.project-too-large": "Sorry, this project can't be shared because it's too large. We're working on it!",
"share-menu.upload-failed": "Sorry, we couldn't upload your project at the moment. Please try again or let us know if the problem persists.",
"status.offline": "Offline",

Wyświetl plik

@ -30,6 +30,7 @@ const config = {
},
},
'apps/docs': {
runsAfter: { 'build-api': { in: 'all-packages' } },
cache: {
inputs: [
'app/**/*',

Wyświetl plik

@ -1,7 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"packages": [
"packages/*"
],
"packages": ["packages/*"],
"version": "2.1.0"
}

Wyświetl plik

@ -1,232 +0,0 @@
## API Report File for "@tldraw/assets"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
// @public (undocumented)
export function getBundlerAssetUrls(opts?: AssetUrlOptions): {
readonly fonts: {
readonly monospace: string;
readonly sansSerif: string;
readonly serif: string;
readonly draw: string;
};
readonly icons: {
readonly 'align-bottom-center': string;
readonly 'align-bottom-left': string;
readonly 'align-bottom-right': string;
readonly 'align-bottom': string;
readonly 'align-center-center': string;
readonly 'align-center-horizontal': string;
readonly 'align-center-left': string;
readonly 'align-center-right': string;
readonly 'align-center-vertical': string;
readonly 'align-left': string;
readonly 'align-right': string;
readonly 'align-top-center': string;
readonly 'align-top-left': string;
readonly 'align-top-right': string;
readonly 'align-top': string;
readonly 'arrow-left': string;
readonly 'arrowhead-arrow': string;
readonly 'arrowhead-bar': string;
readonly 'arrowhead-diamond': string;
readonly 'arrowhead-dot': string;
readonly 'arrowhead-none': string;
readonly 'arrowhead-square': string;
readonly 'arrowhead-triangle-inverted': string;
readonly 'arrowhead-triangle': string;
readonly 'aspect-ratio': string;
readonly avatar: string;
readonly blob: string;
readonly 'bring-forward': string;
readonly 'bring-to-front': string;
readonly check: string;
readonly 'checkbox-checked': string;
readonly 'checkbox-empty': string;
readonly 'chevron-down': string;
readonly 'chevron-left': string;
readonly 'chevron-right': string;
readonly 'chevron-up': string;
readonly 'chevrons-ne': string;
readonly 'chevrons-sw': string;
readonly 'clipboard-copy': string;
readonly code: string;
readonly collab: string;
readonly color: string;
readonly comment: string;
readonly 'cross-2': string;
readonly cross: string;
readonly 'dash-dashed': string;
readonly 'dash-dotted': string;
readonly 'dash-draw': string;
readonly 'dash-solid': string;
readonly discord: string;
readonly 'distribute-horizontal': string;
readonly 'distribute-vertical': string;
readonly dot: string;
readonly 'dots-horizontal': string;
readonly 'dots-vertical': string;
readonly 'drag-handle-dots': string;
readonly duplicate: string;
readonly edit: string;
readonly 'external-link': string;
readonly file: string;
readonly 'fill-none': string;
readonly 'fill-pattern': string;
readonly 'fill-semi': string;
readonly 'fill-solid': string;
readonly follow: string;
readonly following: string;
readonly 'font-draw': string;
readonly 'font-mono': string;
readonly 'font-sans': string;
readonly 'font-serif': string;
readonly 'geo-arrow-down': string;
readonly 'geo-arrow-left': string;
readonly 'geo-arrow-right': string;
readonly 'geo-arrow-up': string;
readonly 'geo-check-box': string;
readonly 'geo-diamond': string;
readonly 'geo-ellipse': string;
readonly 'geo-hexagon': string;
readonly 'geo-octagon': string;
readonly 'geo-oval': string;
readonly 'geo-pentagon': string;
readonly 'geo-rectangle': string;
readonly 'geo-rhombus-2': string;
readonly 'geo-rhombus': string;
readonly 'geo-star': string;
readonly 'geo-trapezoid': string;
readonly 'geo-triangle': string;
readonly 'geo-x-box': string;
readonly github: string;
readonly group: string;
readonly hidden: string;
readonly image: string;
readonly 'info-circle': string;
readonly leading: string;
readonly link: string;
readonly 'lock-small': string;
readonly lock: string;
readonly menu: string;
readonly minus: string;
readonly mixed: string;
readonly pack: string;
readonly page: string;
readonly plus: string;
readonly 'question-mark-circle': string;
readonly 'question-mark': string;
readonly redo: string;
readonly 'reset-zoom': string;
readonly 'rotate-ccw': string;
readonly 'rotate-cw': string;
readonly ruler: string;
readonly search: string;
readonly 'send-backward': string;
readonly 'send-to-back': string;
readonly 'settings-horizontal': string;
readonly 'settings-vertical-1': string;
readonly 'settings-vertical': string;
readonly 'share-1': string;
readonly 'share-2': string;
readonly 'size-extra-large': string;
readonly 'size-large': string;
readonly 'size-medium': string;
readonly 'size-small': string;
readonly 'spline-cubic': string;
readonly 'spline-line': string;
readonly 'stack-horizontal': string;
readonly 'stack-vertical': string;
readonly 'stretch-horizontal': string;
readonly 'stretch-vertical': string;
readonly 'text-align-center': string;
readonly 'text-align-justify': string;
readonly 'text-align-left': string;
readonly 'text-align-right': string;
readonly 'tool-arrow': string;
readonly 'tool-embed': string;
readonly 'tool-eraser': string;
readonly 'tool-frame': string;
readonly 'tool-hand': string;
readonly 'tool-highlighter': string;
readonly 'tool-line': string;
readonly 'tool-media': string;
readonly 'tool-note': string;
readonly 'tool-pencil': string;
readonly 'tool-pointer': string;
readonly 'tool-text': string;
readonly trash: string;
readonly 'triangle-down': string;
readonly 'triangle-up': string;
readonly twitter: string;
readonly undo: string;
readonly ungroup: string;
readonly 'unlock-small': string;
readonly unlock: string;
readonly visible: string;
readonly 'warning-triangle': string;
readonly 'zoom-in': string;
readonly 'zoom-out': string;
};
readonly translations: {
readonly ar: string;
readonly ca: string;
readonly da: string;
readonly de: string;
readonly en: string;
readonly es: string;
readonly fa: string;
readonly fi: string;
readonly fr: string;
readonly gl: string;
readonly he: string;
readonly 'hi-in': string;
readonly hu: string;
readonly it: string;
readonly ja: string;
readonly 'ko-kr': string;
readonly ku: string;
readonly languages: string;
readonly main: string;
readonly my: string;
readonly ne: string;
readonly no: string;
readonly pl: string;
readonly 'pt-br': string;
readonly 'pt-pt': string;
readonly ro: string;
readonly ru: string;
readonly sv: string;
readonly te: string;
readonly th: string;
readonly tr: string;
readonly uk: string;
readonly vi: string;
readonly 'zh-cn': string;
readonly 'zh-tw': string;
};
readonly embedIcons: {
readonly codepen: string;
readonly codesandbox: string;
readonly excalidraw: string;
readonly felt: string;
readonly figma: string;
readonly github_gist: string;
readonly google_calendar: string;
readonly google_maps: string;
readonly google_slides: string;
readonly observable: string;
readonly replit: string;
readonly scratch: string;
readonly spotify: string;
readonly tldraw: string;
readonly vimeo: string;
readonly youtube: string;
};
};
// (No @packageDocumentation comment for this package)
```

Wyświetl plik

@ -0,0 +1,26 @@
{
"name": "@tldraw/dotcom-shared",
"version": "2.0.0",
"private": true,
"/* NOTE */": "These `main` and `types` fields are rewritten by the build script. They are not the actual values we publish",
"main": "./src/index.ts",
"types": "./.tsbuild/index.d.ts",
"/* GOTCHA */": "files will include ./dist and index.d.ts by default, add any others you want to include in here",
"files": [],
"dependencies": {
"tldraw": "workspace:*"
},
"peerDependencies": {
"react": "^18",
"react-dom": "^18"
},
"scripts": {
"test-ci": "lazy inherit",
"test": "yarn run -T jest",
"lint": "yarn run -T tsx ../../scripts/lint.ts"
},
"jest": {
"preset": "config/jest/node",
"testEnvironment": "jsdom"
}
}

Wyświetl plik

@ -0,0 +1,3 @@
it('works', () => {
// we need a test for jest to pass.
})

Wyświetl plik

@ -0,0 +1,16 @@
export {
READ_ONLY_LEGACY_PREFIX,
READ_ONLY_PREFIX,
ROOM_OPEN_MODE,
ROOM_PREFIX,
RoomOpenModeToPath,
SNAPSHOT_PREFIX,
type RoomOpenMode,
} from './routes'
export type {
CreateRoomRequestBody,
CreateSnapshotRequestBody,
CreateSnapshotResponseBody,
GetReadonlySlugResponseBody,
Snapshot,
} from './types'

Wyświetl plik

@ -0,0 +1,23 @@
/** @public */
export const ROOM_OPEN_MODE = {
READ_ONLY: 'readonly',
READ_ONLY_LEGACY: 'readonly-legacy',
READ_WRITE: 'read-write',
} as const
export type RoomOpenMode = (typeof ROOM_OPEN_MODE)[keyof typeof ROOM_OPEN_MODE]
/** @public */
export const READ_ONLY_PREFIX = 'ro'
/** @public */
export const READ_ONLY_LEGACY_PREFIX = 'v'
/** @public */
export const ROOM_PREFIX = 'r'
/** @public */
export const SNAPSHOT_PREFIX = 's'
/** @public */
export const RoomOpenModeToPath: Record<RoomOpenMode, string> = {
[ROOM_OPEN_MODE.READ_ONLY]: READ_ONLY_PREFIX,
[ROOM_OPEN_MODE.READ_ONLY_LEGACY]: READ_ONLY_LEGACY_PREFIX,
[ROOM_OPEN_MODE.READ_WRITE]: ROOM_PREFIX,
}

Wyświetl plik

@ -0,0 +1,29 @@
import { SerializedSchema, SerializedStore, TLRecord } from 'tldraw'
export type Snapshot = {
schema: SerializedSchema
snapshot: SerializedStore<TLRecord>
}
export type CreateRoomRequestBody = {
origin: string
snapshot: Snapshot
}
export type CreateSnapshotRequestBody = {
schema: SerializedSchema
snapshot: SerializedStore<TLRecord>
parent_slug?: string | string[] | undefined
}
export type CreateSnapshotResponseBody =
| {
error: false
roomId: string
}
| {
error: true
message: string
}
export type GetReadonlySlugResponseBody = { slug: string; isLegacy: boolean }

Wyświetl plik

@ -0,0 +1,14 @@
{
"extends": "../../config/tsconfig.base.json",
"include": ["src"],
"exclude": ["node_modules", ".tsbuild*"],
"compilerOptions": {
"outDir": "./.tsbuild",
"rootDir": "src"
},
"references": [
{
"path": "../tldraw"
}
]
}

Wyświetl plik

@ -29,10 +29,12 @@ import { default as React_2 } from 'react';
import * as React_3 from 'react';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { RecordsDiff } from '@tldraw/store';
import { SerializedSchema } from '@tldraw/store';
import { SerializedStore } from '@tldraw/store';
import { ShapeProps } from '@tldraw/tlschema';
import { Signal } from '@tldraw/state';
import { Store } from '@tldraw/store';
import { StoreSchema } from '@tldraw/store';
import { StoreSnapshot } from '@tldraw/store';
import { StyleProp } from '@tldraw/tlschema';
@ -375,7 +377,7 @@ export function counterClockwiseAngleDist(a0: number, a1: number): number;
export function createSessionStateSnapshotSignal(store: TLStore): Signal<null | TLSessionStateSnapshot>;
// @public
export function createTLStore({ initialData, defaultName, ...rest }: TLStoreOptions): TLStore;
export function createTLStore({ initialData, defaultName, id, ...rest }: TLStoreOptions): TLStore;
// @public (undocumented)
export function createTLUser(opts?: {
@ -602,7 +604,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}): this;
bail(): this;
bailToMark(id: string): this;
batch(fn: () => void): this;
batch(fn: () => void, opts?: TLHistoryBatchOptions): this;
bringForward(shapes: TLShape[] | TLShapeId[]): this;
bringToFront(shapes: TLShape[] | TLShapeId[]): this;
cancel(): this;
@ -811,7 +813,7 @@ export class Editor extends EventEmitter<TLEventMap> {
getZoomLevel(): number;
groupShapes(shapes: TLShape[] | TLShapeId[], groupId?: TLShapeId): this;
hasAncestor(shape: TLShape | TLShapeId | undefined, ancestorId: TLShapeId): boolean;
readonly history: HistoryManager<this>;
readonly history: HistoryManager<TLRecord>;
inputs: {
buttons: Set<number>;
keys: Set<string>;
@ -833,6 +835,7 @@ export class Editor extends EventEmitter<TLEventMap> {
isPointing: boolean;
};
interrupt(): this;
isAncestorSelected(shape: TLShape | TLShapeId): boolean;
isIn(path: string): boolean;
isInAny(...paths: string[]): boolean;
isPointInShape(shape: TLShape | TLShapeId, point: VecLike, opts?: {
@ -846,9 +849,9 @@ export class Editor extends EventEmitter<TLEventMap> {
isShapeOrAncestorLocked(shape?: TLShape): boolean;
// (undocumented)
isShapeOrAncestorLocked(id?: TLShapeId): boolean;
mark(markId?: string, onUndo?: boolean, onRedo?: boolean): this;
mark(markId?: string): this;
moveShapesToPage(shapes: TLShape[] | TLShapeId[], pageId: TLPageId): this;
nudgeShapes(shapes: TLShape[] | TLShapeId[], offset: VecLike, historyOptions?: TLCommandHistoryOptions): this;
nudgeShapes(shapes: TLShape[] | TLShapeId[], offset: VecLike): this;
packShapes(shapes: TLShape[] | TLShapeId[], gap: number): this;
pageToScreen(point: VecLike): {
x: number;
@ -877,7 +880,7 @@ export class Editor extends EventEmitter<TLEventMap> {
registerExternalContentHandler<T extends TLExternalContent['type']>(type: T, handler: ((info: T extends TLExternalContent['type'] ? TLExternalContent & {
type: T;
} : TLExternalContent) => void) | null): this;
renamePage(page: TLPage | TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this;
renamePage(page: TLPage | TLPageId, name: string): this;
renderingBoundsMargin: number;
reparentShapes(shapes: TLShape[] | TLShapeId[], parentId: TLParentId, insertIndex?: IndexKey): this;
resetZoom(point?: Vec, animation?: TLAnimationOptions): this;
@ -897,7 +900,7 @@ export class Editor extends EventEmitter<TLEventMap> {
sendToBack(shapes: TLShape[] | TLShapeId[]): this;
setCamera(point: VecLike, animation?: TLAnimationOptions): this;
setCroppingShape(shape: null | TLShape | TLShapeId): this;
setCurrentPage(page: TLPage | TLPageId, historyOptions?: TLCommandHistoryOptions): this;
setCurrentPage(page: TLPage | TLPageId): this;
setCurrentTool(id: string, info?: {}): this;
setCursor: (cursor: Partial<TLCursor>) => this;
setEditingShape(shape: null | TLShape | TLShapeId): this;
@ -905,11 +908,11 @@ export class Editor extends EventEmitter<TLEventMap> {
setFocusedGroup(shape: null | TLGroupShape | TLShapeId): this;
setHintingShapes(shapes: TLShape[] | TLShapeId[]): this;
setHoveredShape(shape: null | TLShape | TLShapeId): this;
setOpacityForNextShapes(opacity: number, historyOptions?: TLCommandHistoryOptions): this;
setOpacityForSelectedShapes(opacity: number, historyOptions?: TLCommandHistoryOptions): this;
setSelectedShapes(shapes: TLShape[] | TLShapeId[], historyOptions?: TLCommandHistoryOptions): this;
setStyleForNextShapes<T>(style: StyleProp<T>, value: T, historyOptions?: TLCommandHistoryOptions): this;
setStyleForSelectedShapes<S extends StyleProp<any>>(style: S, value: StylePropValue<S>, historyOptions?: TLCommandHistoryOptions): this;
setOpacityForNextShapes(opacity: number, historyOptions?: TLHistoryBatchOptions): this;
setOpacityForSelectedShapes(opacity: number): this;
setSelectedShapes(shapes: TLShape[] | TLShapeId[]): this;
setStyleForNextShapes<T>(style: StyleProp<T>, value: T, historyOptions?: TLHistoryBatchOptions): this;
setStyleForSelectedShapes<S extends StyleProp<any>>(style: S, value: StylePropValue<S>): this;
shapeUtils: {
readonly [K in string]?: ShapeUtil<TLUnknownShape>;
};
@ -938,14 +941,16 @@ export class Editor extends EventEmitter<TLEventMap> {
// (undocumented)
ungroupShapes(ids: TLShape[]): this;
updateAssets(assets: TLAssetPartial[]): this;
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: TLCommandHistoryOptions): this;
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions): this;
// (undocumented)
_updateCurrentPageState: (partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions) => void;
updateDocumentSettings(settings: Partial<TLDocument>): this;
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, historyOptions?: TLCommandHistoryOptions): this;
updatePage(partial: RequiredKeys<TLPage, 'id'>, historyOptions?: TLCommandHistoryOptions): this;
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, historyOptions?: TLHistoryBatchOptions): this;
updatePage(partial: RequiredKeys<TLPage, 'id'>): this;
// @internal
updateRenderingBounds(): this;
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined, historyOptions?: TLCommandHistoryOptions): this;
updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[], historyOptions?: TLCommandHistoryOptions): this;
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined): this;
updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[]): this;
updateViewportScreenBounds(screenBounds: Box, center?: boolean): this;
readonly user: UserPreferencesManager;
visitDescendants(parent: TLPage | TLParentId | TLShape, visitor: (id: TLShapeId) => false | void): this;
@ -1209,6 +1214,55 @@ export function hardResetEditor(): void;
// @internal (undocumented)
export const HASH_PATTERN_ZOOM_NAMES: Record<string, string>;
// @public (undocumented)
export class HistoryManager<R extends UnknownRecord> {
constructor(opts: {
annotateError?: (error: unknown) => void;
store: Store<R>;
});
// (undocumented)
bail: () => this;
// (undocumented)
bailToMark: (id: string) => this;
// (undocumented)
batch: (fn: () => void, opts?: TLHistoryBatchOptions) => this;
// (undocumented)
clear(): void;
// @internal (undocumented)
debug(): {
pendingDiff: {
diff: RecordsDiff<R>;
isEmpty: boolean;
};
redos: (NonNullable<TLHistoryEntry<R>> | undefined)[];
state: HistoryRecorderState;
undos: (NonNullable<TLHistoryEntry<R>> | undefined)[];
};
// (undocumented)
readonly dispose: () => void;
// (undocumented)
getNumRedos(): number;
// (undocumented)
getNumUndos(): number;
// (undocumented)
ignore(fn: () => void): this;
// @internal (undocumented)
_isInBatch: boolean;
// (undocumented)
mark: (id?: string) => string;
// (undocumented)
onBatchComplete: () => void;
// (undocumented)
redo: () => this | undefined;
// @internal (undocumented)
stacks: Atom< {
redos: Stack<TLHistoryEntry<R>>;
undos: Stack<TLHistoryEntry<R>>;
}, unknown>;
// (undocumented)
undo: () => this;
}
// @public (undocumented)
export const HIT_TEST_MARGIN = 8;
@ -1724,6 +1778,17 @@ export class SideEffectManager<CTX extends {
constructor(editor: CTX);
// (undocumented)
editor: CTX;
// @internal
register(handlersByType: {
[R in TLRecord as R['typeName']]?: {
afterChange?: TLAfterChangeHandler<R>;
afterCreate?: TLAfterCreateHandler<R>;
afterDelete?: TLAfterDeleteHandler<R>;
beforeChange?: TLBeforeChangeHandler<R>;
beforeCreate?: TLBeforeCreateHandler<R>;
beforeDelete?: TLBeforeDeleteHandler<R>;
};
}): () => void;
registerAfterChangeHandler<T extends TLRecord['typeName']>(typeName: T, handler: TLAfterChangeHandler<TLRecord & {
typeName: T;
}>): () => void;
@ -2038,29 +2103,6 @@ export type TLCollaboratorHintProps = {
zoom: number;
};
// @public (undocumented)
export type TLCommand<Name extends string = any, Data = any> = {
preservesRedoStack?: boolean;
data: Data;
name: Name;
type: 'command';
};
// @public (undocumented)
export type TLCommandHandler<Data> = {
squash?: (prevData: Data, nextData: Data) => Data;
do: (data: Data) => void;
redo?: (data: Data) => void;
undo: (data: Data) => void;
};
// @public (undocumented)
export type TLCommandHistoryOptions = Partial<{
preservesRedoStack: boolean;
squashing: boolean;
ephemeral: boolean;
}>;
// @public (undocumented)
export type TLCompleteEvent = (info: TLCompleteEventInfo) => void;
@ -2194,17 +2236,6 @@ export type TLEventInfo = TLCancelEventInfo | TLClickEventInfo | TLCompleteEvent
// @public (undocumented)
export interface TLEventMap {
// (undocumented)
'change-history': [{
markId?: string;
reason: 'bail';
} | {
reason: 'push' | 'redo' | 'undo';
}];
// (undocumented)
'mark-history': [{
id: string;
}];
// (undocumented)
'max-shapes': [{
count: number;
@ -2317,17 +2348,6 @@ export type TLHandlesProps = {
children: ReactNode;
};
// @public (undocumented)
export type TLHistoryEntry = TLCommand | TLHistoryMark;
// @public (undocumented)
export type TLHistoryMark = {
id: string;
onRedo: boolean;
onUndo: boolean;
type: 'STOP';
};
// @public (undocumented)
export type TLInterruptEvent = (info: TLInterruptEventInfo) => void;
@ -2611,6 +2631,7 @@ export type TLStoreEventInfo = HistoryEntry<TLRecord>;
// @public (undocumented)
export type TLStoreOptions = {
defaultName?: string;
id?: string;
initialData?: SerializedStore<TLRecord>;
} & ({
migrations?: readonly MigrationSequence[];

Plik diff jest za duży Load Diff

Wyświetl plik

@ -17,7 +17,6 @@ export {
type Atom,
type Signal,
} from '@tldraw/state'
export type { TLCommandHistoryOptions } from './lib/editor/types/history-types'
// eslint-disable-next-line local/no-export-star
export * from '@tldraw/store'
// eslint-disable-next-line local/no-export-star
@ -131,6 +130,7 @@ export {
type TLEditorOptions,
type TLResizeShapeOptions,
} from './lib/editor/Editor'
export { HistoryManager } from './lib/editor/managers/HistoryManager'
export type {
SideEffectManager,
TLAfterChangeHandler,
@ -235,12 +235,6 @@ export {
type TLExternalContent,
type TLExternalContentSource,
} from './lib/editor/types/external-content'
export {
type TLCommand,
type TLCommandHandler,
type TLHistoryEntry,
type TLHistoryMark,
} from './lib/editor/types/history-types'
export { type RequiredKeys, type TLSvgOptions } from './lib/editor/types/misc-types'
export { type TLResizeHandle, type TLSelectionHandle } from './lib/editor/types/selection-types'
export { ContainerProvider, useContainer } from './lib/hooks/useContainer'

Wyświetl plik

@ -380,8 +380,11 @@ function useOnMount(onMount?: TLOnMountHandler) {
const editor = useEditor()
const onMountEvent = useEvent((editor: Editor) => {
const teardown = onMount?.(editor)
editor.emit('mount')
let teardown: (() => void) | void = undefined
editor.history.ignore(() => {
teardown = onMount?.(editor)
editor.emit('mount')
})
window.tldrawReady = true
return teardown
})

Wyświetl plik

@ -14,6 +14,7 @@ import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from './defaultShape
export type TLStoreOptions = {
initialData?: SerializedStore<TLRecord>
defaultName?: string
id?: string
} & (
| { shapeUtils?: readonly TLAnyShapeUtilConstructor[]; migrations?: readonly MigrationSequence[] }
| { schema?: StoreSchema<TLRecord, TLStoreProps> }
@ -28,7 +29,12 @@ export type TLStoreEventInfo = HistoryEntry<TLRecord>
* @param opts - Options for creating the store.
*
* @public */
export function createTLStore({ initialData, defaultName = '', ...rest }: TLStoreOptions): TLStore {
export function createTLStore({
initialData,
defaultName = '',
id,
...rest
}: TLStoreOptions): TLStore {
const schema =
'schema' in rest && rest.schema
? // we have a schema
@ -42,6 +48,7 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
})
return new Store({
id,
schema,
initialData,
props: {

Wyświetl plik

@ -1,92 +1,75 @@
import { TLCommandHistoryOptions } from '../types/history-types'
import { BaseRecord, RecordId, Store, StoreSchema, createRecordType } from '@tldraw/store'
import { TLHistoryBatchOptions } from '../types/history-types'
import { HistoryManager } from './HistoryManager'
import { stack } from './Stack'
interface TestRecord extends BaseRecord<'test', TestRecordId> {
value: number | string
}
type TestRecordId = RecordId<TestRecord>
const testSchema = StoreSchema.create<TestRecord, null>({
test: createRecordType<TestRecord>('test', { scope: 'document' }),
})
const ids = {
count: testSchema.types.test.createId('count'),
name: testSchema.types.test.createId('name'),
age: testSchema.types.test.createId('age'),
a: testSchema.types.test.createId('a'),
b: testSchema.types.test.createId('b'),
}
function createCounterHistoryManager() {
const manager = new HistoryManager({ emit: () => void null }, () => {
return
})
const state = {
count: 0,
name: 'David',
age: 35,
const store = new Store({ schema: testSchema, props: null })
store.put([
testSchema.types.test.create({ id: ids.count, value: 0 }),
testSchema.types.test.create({ id: ids.name, value: 'David' }),
testSchema.types.test.create({ id: ids.age, value: 35 }),
])
const manager = new HistoryManager<TestRecord>({ store })
function getCount() {
return store.get(ids.count)!.value as number
}
function getName() {
return store.get(ids.name)!.value as string
}
function getAge() {
return store.get(ids.age)!.value as number
}
function _setCount(n: number) {
store.update(ids.count, (c) => ({ ...c, value: n }))
}
function _setName(name: string) {
store.update(ids.name, (c) => ({ ...c, value: name }))
}
function _setAge(age: number) {
store.update(ids.age, (c) => ({ ...c, value: age }))
}
const increment = manager.createCommand(
'increment',
(n = 1, squashing = false) => ({
data: { n },
squashing,
}),
{
do: ({ n }) => {
state.count += n
},
undo: ({ n }) => {
state.count -= n
},
squash: ({ n: n1 }, { n: n2 }) => ({ n: n1 + n2 }),
}
)
const decrement = manager.createCommand(
'decrement',
(n = 1, squashing = false) => ({
data: { n },
squashing,
}),
{
do: ({ n }) => {
state.count -= n
},
undo: ({ n }) => {
state.count += n
},
squash: ({ n: n1 }, { n: n2 }) => ({ n: n1 + n2 }),
}
)
const increment = (n = 1) => {
_setCount(getCount() + n)
}
const setName = manager.createCommand(
'setName',
(name = 'David') => ({
data: { name, prev: state.name },
ephemeral: true,
}),
{
do: ({ name }) => {
state.name = name
},
undo: ({ prev }) => {
state.name = prev
},
}
)
const decrement = (n = 1) => {
_setCount(getCount() - n)
}
const setAge = manager.createCommand(
'setAge',
(age = 35) => ({
data: { age, prev: state.age },
preservesRedoStack: true,
}),
{
do: ({ age }) => {
state.age = age
},
undo: ({ prev }) => {
state.age = prev
},
}
)
const setName = (name = 'David') => {
manager.ignore(() => _setName(name))
}
const incrementTwice = manager.createCommand('incrementTwice', () => ({ data: {} }), {
do: () => {
const setAge = (age = 35) => {
manager.batch(() => _setAge(age), { history: 'record-preserveRedoStack' })
}
const incrementTwice = () => {
manager.batch(() => {
increment()
increment()
},
undo: () => {
decrement()
decrement()
},
})
})
}
return {
increment,
@ -95,9 +78,9 @@ function createCounterHistoryManager() {
setName,
setAge,
history: manager,
getCount: () => state.count,
getName: () => state.name,
getAge: () => state.age,
getCount,
getName,
getAge,
}
}
@ -116,9 +99,9 @@ describe(HistoryManager, () => {
editor.decrement()
expect(editor.getCount()).toBe(3)
const undos = [...editor.history._undos.get()]
const undos = [...editor.history.stacks.get().undos]
const parsedUndos = JSON.parse(JSON.stringify(undos))
editor.history._undos.set(stack(parsedUndos))
editor.history.stacks.update(({ redos }) => ({ undos: stack(parsedUndos), redos }))
editor.history.undo()
@ -200,17 +183,16 @@ describe(HistoryManager, () => {
editor.history.mark('stop at 1')
expect(editor.getCount()).toBe(1)
editor.increment(1, true)
editor.increment(1, true)
editor.increment(1, true)
editor.increment(1, true)
editor.increment(1)
editor.increment(1)
editor.increment(1)
editor.increment(1)
expect(editor.getCount()).toBe(5)
expect(editor.history.getNumUndos()).toBe(3)
})
it('allows ephemeral commands that do not affect the stack', () => {
it('allows ignore commands that do not affect the stack', () => {
editor.increment()
editor.history.mark('stop at 1')
editor.increment()
@ -263,7 +245,7 @@ describe(HistoryManager, () => {
editor.history.mark('2')
editor.incrementTwice()
editor.incrementTwice()
expect(editor.history.getNumUndos()).toBe(5)
expect(editor.history.getNumUndos()).toBe(4)
expect(editor.getCount()).toBe(6)
editor.history.bail()
expect(editor.getCount()).toBe(2)
@ -289,58 +271,35 @@ describe(HistoryManager, () => {
})
describe('history options', () => {
let manager: HistoryManager<any>
let state: { a: number; b: number }
let manager: HistoryManager<TestRecord>
let setA: (n: number, historyOptions?: TLCommandHistoryOptions) => any
let setB: (n: number, historyOptions?: TLCommandHistoryOptions) => any
let getState: () => { a: number; b: number }
let setA: (n: number, historyOptions?: TLHistoryBatchOptions) => any
let setB: (n: number, historyOptions?: TLHistoryBatchOptions) => any
beforeEach(() => {
manager = new HistoryManager({ emit: () => void null }, () => {
return
})
const store = new Store({ schema: testSchema, props: null })
store.put([
testSchema.types.test.create({ id: ids.a, value: 0 }),
testSchema.types.test.create({ id: ids.b, value: 0 }),
])
state = {
a: 0,
b: 0,
manager = new HistoryManager<TestRecord>({ store })
getState = () => {
return { a: store.get(ids.a)!.value as number, b: store.get(ids.b)!.value as number }
}
setA = manager.createCommand(
'setA',
(n: number, historyOptions?: TLCommandHistoryOptions) => ({
data: { next: n, prev: state.a },
...historyOptions,
}),
{
do: ({ next }) => {
state = { ...state, a: next }
},
undo: ({ prev }) => {
state = { ...state, a: prev }
},
squash: ({ prev }, { next }) => ({ prev, next }),
}
)
setA = (n: number, historyOptions?: TLHistoryBatchOptions) => {
manager.batch(() => store.update(ids.a, (s) => ({ ...s, value: n })), historyOptions)
}
setB = manager.createCommand(
'setB',
(n: number, historyOptions?: TLCommandHistoryOptions) => ({
data: { next: n, prev: state.b },
...historyOptions,
}),
{
do: ({ next }) => {
state = { ...state, b: next }
},
undo: ({ prev }) => {
state = { ...state, b: prev }
},
squash: ({ prev }, { next }) => ({ prev, next }),
}
)
setB = (n: number, historyOptions?: TLHistoryBatchOptions) => {
manager.batch(() => store.update(ids.b, (s) => ({ ...s, value: n })), historyOptions)
}
})
it('sets, undoes, redoes', () => {
it('undos, redoes, separate marks', () => {
manager.mark()
setA(1)
manager.mark()
@ -348,18 +307,18 @@ describe('history options', () => {
manager.mark()
setB(2)
expect(state).toMatchObject({ a: 1, b: 2 })
expect(getState()).toMatchObject({ a: 1, b: 2 })
manager.undo()
expect(state).toMatchObject({ a: 1, b: 1 })
expect(getState()).toMatchObject({ a: 1, b: 1 })
manager.redo()
expect(state).toMatchObject({ a: 1, b: 2 })
expect(getState()).toMatchObject({ a: 1, b: 2 })
})
it('sets, undoes, redoes', () => {
it('undos, redos, squashing', () => {
manager.mark()
setA(1)
manager.mark()
@ -369,71 +328,107 @@ describe('history options', () => {
setB(3)
setB(4)
expect(state).toMatchObject({ a: 1, b: 4 })
expect(getState()).toMatchObject({ a: 1, b: 4 })
manager.undo()
expect(state).toMatchObject({ a: 1, b: 1 })
expect(getState()).toMatchObject({ a: 1, b: 1 })
manager.redo()
expect(state).toMatchObject({ a: 1, b: 4 })
expect(getState()).toMatchObject({ a: 1, b: 4 })
})
it('sets ephemeral, undoes, redos', () => {
it('undos, redos, ignore', () => {
manager.mark()
setA(1)
manager.mark()
setB(1) // B 0->1
manager.mark()
setB(2, { ephemeral: true }) // B 0->2, but ephemeral
setB(2, { history: 'ignore' }) // B 0->2, but ignore
expect(state).toMatchObject({ a: 1, b: 2 })
expect(getState()).toMatchObject({ a: 1, b: 2 })
manager.undo() // undoes B 2->0
expect(state).toMatchObject({ a: 1, b: 0 })
expect(getState()).toMatchObject({ a: 1, b: 0 })
manager.redo() // redoes B 0->1, but not B 1-> 2
expect(state).toMatchObject({ a: 1, b: 1 }) // no change, b 1->2 was ephemeral
expect(getState()).toMatchObject({ a: 1, b: 1 }) // no change, b 1->2 was ignore
})
it('sets squashing, undoes, redos', () => {
it('squashing, undos, redos', () => {
manager.mark()
setA(1)
manager.mark()
setB(1)
setB(2, { squashing: true }) // squashes with the previous command
setB(3, { squashing: true }) // squashes with the previous command
setB(2) // squashes with the previous command
setB(3) // squashes with the previous command
expect(state).toMatchObject({ a: 1, b: 3 })
expect(getState()).toMatchObject({ a: 1, b: 3 })
manager.undo()
expect(state).toMatchObject({ a: 1, b: 0 })
expect(getState()).toMatchObject({ a: 1, b: 0 })
manager.redo()
expect(state).toMatchObject({ a: 1, b: 3 })
expect(getState()).toMatchObject({ a: 1, b: 3 })
})
it('sets squashing and ephemeral, undoes, redos', () => {
it('squashing, undos, redos, ignore', () => {
manager.mark()
setA(1)
manager.mark()
setB(1)
setB(2, { squashing: true }) // squashes with the previous command
setB(3, { squashing: true, ephemeral: true }) // squashes with the previous command
setB(2) // squashes with the previous command
setB(3, { history: 'ignore' }) // squashes with the previous command
expect(state).toMatchObject({ a: 1, b: 3 })
expect(getState()).toMatchObject({ a: 1, b: 3 })
manager.undo()
expect(state).toMatchObject({ a: 1, b: 0 })
expect(getState()).toMatchObject({ a: 1, b: 0 })
manager.redo()
expect(state).toMatchObject({ a: 1, b: 2 }) // B2->3 was ephemeral
expect(getState()).toMatchObject({ a: 1, b: 2 }) // B2->3 was ignore
})
it('nested ignore', () => {
manager.mark()
manager.batch(
() => {
setA(1)
// even though we set this to record, it will still be ignored
manager.batch(() => setB(1), { history: 'record' })
setA(2)
},
{ history: 'ignore' }
)
expect(getState()).toMatchObject({ a: 2, b: 1 })
// changes were ignored:
manager.undo()
expect(getState()).toMatchObject({ a: 2, b: 1 })
manager.mark()
manager.batch(
() => {
setA(3)
manager.batch(() => setB(2), { history: 'ignore' })
},
{ history: 'record-preserveRedoStack' }
)
expect(getState()).toMatchObject({ a: 3, b: 2 })
// changes to A were recorded, but changes to B were ignore:
manager.undo()
expect(getState()).toMatchObject({ a: 2, b: 2 })
// We can still redo because we preserved the redo stack:
manager.redo()
expect(getState()).toMatchObject({ a: 3, b: 2 })
})
})

Wyświetl plik

@ -1,156 +1,124 @@
import { atom, transact } from '@tldraw/state'
import { devFreeze } from '@tldraw/store'
import {
RecordsDiff,
Store,
UnknownRecord,
createEmptyRecordsDiff,
isRecordsDiffEmpty,
reverseRecordsDiff,
squashRecordDiffsMutable,
} from '@tldraw/store'
import { exhaustiveSwitchError, noop } from '@tldraw/utils'
import { uniqueId } from '../../utils/uniqueId'
import { TLCommandHandler, TLCommandHistoryOptions, TLHistoryEntry } from '../types/history-types'
import { Stack, stack } from './Stack'
import { TLHistoryBatchOptions, TLHistoryEntry } from '../types/history-types'
import { stack } from './Stack'
type CommandFn<Data> = (...args: any[]) =>
| ({
data: Data
} & TLCommandHistoryOptions)
| null
| undefined
| void
enum HistoryRecorderState {
Recording = 'recording',
RecordingPreserveRedoStack = 'recordingPreserveRedoStack',
Paused = 'paused',
}
type ExtractData<Fn> = Fn extends CommandFn<infer Data> ? Data : never
type ExtractArgs<Fn> = Parameters<Extract<Fn, (...args: any[]) => any>>
/** @public */
export class HistoryManager<R extends UnknownRecord> {
private readonly store: Store<R>
export class HistoryManager<
CTX extends {
emit: (name: 'change-history' | 'mark-history', ...args: any) => void
},
> {
_undos = atom<Stack<TLHistoryEntry>>('HistoryManager.undos', stack()) // Updated by each action that includes and undo
_redos = atom<Stack<TLHistoryEntry>>('HistoryManager.redos', stack()) // Updated when a user undoes
_batchDepth = 0 // A flag for whether the user is in a batch operation
readonly dispose: () => void
constructor(
private readonly ctx: CTX,
private readonly annotateError: (error: unknown) => void
) {}
private state: HistoryRecorderState = HistoryRecorderState.Recording
private readonly pendingDiff = new PendingDiff<R>()
/** @internal */
stacks = atom(
'HistoryManager.stacks',
{
undos: stack<TLHistoryEntry<R>>(),
redos: stack<TLHistoryEntry<R>>(),
},
{
isEqual: (a, b) => a.undos === b.undos && a.redos === b.redos,
}
)
private readonly annotateError: (error: unknown) => void
constructor(opts: { store: Store<R>; annotateError?: (error: unknown) => void }) {
this.store = opts.store
this.annotateError = opts.annotateError ?? noop
this.dispose = this.store.addHistoryInterceptor((entry, source) => {
if (source !== 'user') return
switch (this.state) {
case HistoryRecorderState.Recording:
this.pendingDiff.apply(entry.changes)
this.stacks.update(({ undos }) => ({ undos, redos: stack() }))
break
case HistoryRecorderState.RecordingPreserveRedoStack:
this.pendingDiff.apply(entry.changes)
break
case HistoryRecorderState.Paused:
break
default:
exhaustiveSwitchError(this.state)
}
})
}
private flushPendingDiff() {
if (this.pendingDiff.isEmpty()) return
const diff = this.pendingDiff.clear()
this.stacks.update(({ undos, redos }) => ({
undos: undos.push({ type: 'diff', diff }),
redos,
}))
}
onBatchComplete: () => void = () => void null
private _commands: Record<string, TLCommandHandler<any>> = {}
getNumUndos() {
return this._undos.get().length
return this.stacks.get().undos.length + (this.pendingDiff.isEmpty() ? 0 : 1)
}
getNumRedos() {
return this._redos.get().length
}
createCommand = <Name extends string, Constructor extends CommandFn<any>>(
name: Name,
constructor: Constructor,
handle: TLCommandHandler<ExtractData<Constructor>>
) => {
if (this._commands[name]) {
throw new Error(`Duplicate command: ${name}`)
}
this._commands[name] = handle
const exec = (...args: ExtractArgs<Constructor>) => {
if (!this._batchDepth) {
// If we're not batching, run again in a batch
this.batch(() => exec(...args))
return this.ctx
}
const result = constructor(...args)
if (!result) {
return this.ctx
}
const { data, ephemeral, squashing, preservesRedoStack } = result
this.ignoringUpdates((undos, redos) => {
handle.do(data)
return { undos, redos }
})
if (!ephemeral) {
const prev = this._undos.get().head
if (
squashing &&
prev &&
prev.type === 'command' &&
prev.name === name &&
prev.preservesRedoStack === preservesRedoStack
) {
// replace the last command with a squashed version
this._undos.update((undos) =>
undos.tail.push({
...prev,
data: devFreeze(handle.squash!(prev.data, data)),
})
)
} else {
// add to the undo stack
this._undos.update((undos) =>
undos.push({
type: 'command',
name,
data: devFreeze(data),
preservesRedoStack: preservesRedoStack,
})
)
}
if (!result.preservesRedoStack) {
this._redos.set(stack())
}
this.ctx.emit('change-history', { reason: 'push' })
}
return this.ctx
}
return exec
return this.stacks.get().redos.length
}
batch = (fn: () => void) => {
/** @internal */
_isInBatch = false
batch = (fn: () => void, opts?: TLHistoryBatchOptions) => {
const previousState = this.state
// we move to the new state only if we haven't explicitly paused
if (previousState !== HistoryRecorderState.Paused && opts?.history) {
this.state = modeToState[opts.history]
}
try {
this._batchDepth++
if (this._batchDepth === 1) {
transact(() => {
const mostRecentAction = this._undos.get().head
fn()
if (mostRecentAction !== this._undos.get().head) {
this.onBatchComplete()
}
})
} else {
if (this._isInBatch) {
fn()
return this
}
} catch (error) {
this.annotateError(error)
throw error
} finally {
this._batchDepth--
}
return this
this._isInBatch = true
try {
transact(() => {
fn()
this.onBatchComplete()
})
} catch (error) {
this.annotateError(error)
throw error
} finally {
this._isInBatch = false
}
return this
} finally {
this.state = previousState
}
}
private ignoringUpdates = (
fn: (
undos: Stack<TLHistoryEntry>,
redos: Stack<TLHistoryEntry>
) => { undos: Stack<TLHistoryEntry>; redos: Stack<TLHistoryEntry> }
) => {
let undos = this._undos.get()
let redos = this._redos.get()
this._undos.set(stack())
this._redos.set(stack())
try {
;({ undos, redos } = transact(() => fn(undos, redos)))
} finally {
this._undos.set(undos)
this._redos.set(redos)
}
ignore(fn: () => void) {
return this.batch(fn, { history: 'ignore' })
}
// History
@ -161,62 +129,66 @@ export class HistoryManager<
pushToRedoStack: boolean
toMark?: string
}) => {
this.ignoringUpdates((undos, redos) => {
if (undos.length === 0) {
return { undos, redos }
const previousState = this.state
this.state = HistoryRecorderState.Paused
try {
let { undos, redos } = this.stacks.get()
// start by collecting the pending diff (everything since the last mark).
// we'll accumulate the diff to undo in this variable so we can apply it atomically.
const pendingDiff = this.pendingDiff.clear()
const isPendingDiffEmpty = isRecordsDiffEmpty(pendingDiff)
const diffToUndo = reverseRecordsDiff(pendingDiff)
if (pushToRedoStack && !isPendingDiffEmpty) {
redos = redos.push({ type: 'diff', diff: pendingDiff })
}
while (undos.head?.type === 'STOP') {
const mark = undos.head
undos = undos.tail
if (pushToRedoStack) {
redos = redos.push(mark)
}
if (mark.id === toMark) {
this.ctx.emit(
'change-history',
pushToRedoStack ? { reason: 'undo' } : { reason: 'bail', markId: toMark }
)
return { undos, redos }
}
}
if (undos.length === 0) {
this.ctx.emit(
'change-history',
pushToRedoStack ? { reason: 'undo' } : { reason: 'bail', markId: toMark }
)
return { undos, redos }
}
while (undos.head) {
const command = undos.head
undos = undos.tail
if (pushToRedoStack) {
redos = redos.push(command)
}
if (command.type === 'STOP') {
if (command.onUndo && (!toMark || command.id === toMark)) {
this.ctx.emit(
'change-history',
pushToRedoStack ? { reason: 'undo' } : { reason: 'bail', markId: toMark }
)
return { undos, redos }
let didFindMark = false
if (isPendingDiffEmpty) {
// if nothing has happened since the last mark, pop any intermediate marks off the stack
while (undos.head?.type === 'stop') {
const mark = undos.head
undos = undos.tail
if (pushToRedoStack) {
redos = redos.push(mark)
}
if (mark.id === toMark) {
didFindMark = true
break
}
} else {
const handler = this._commands[command.name]
handler.undo(command.data)
}
}
this.ctx.emit(
'change-history',
pushToRedoStack ? { reason: 'undo' } : { reason: 'bail', markId: toMark }
)
return { undos, redos }
})
if (!didFindMark) {
loop: while (undos.head) {
const undo = undos.head
undos = undos.tail
if (pushToRedoStack) {
redos = redos.push(undo)
}
switch (undo.type) {
case 'diff':
squashRecordDiffsMutable(diffToUndo, [reverseRecordsDiff(undo.diff)])
break
case 'stop':
if (!toMark) break loop
if (undo.id === toMark) break loop
break
default:
exhaustiveSwitchError(undo)
}
}
}
this.store.applyDiff(diffToUndo, { ignoreEphemeralKeys: true })
this.store.ensureStoreIsUsable()
this.stacks.set({ undos, redos })
} finally {
this.state = previousState
}
return this
}
@ -228,43 +200,43 @@ export class HistoryManager<
}
redo = () => {
this.ignoringUpdates((undos, redos) => {
const previousState = this.state
this.state = HistoryRecorderState.Paused
try {
this.flushPendingDiff()
let { undos, redos } = this.stacks.get()
if (redos.length === 0) {
return { undos, redos }
return
}
while (redos.head?.type === 'STOP') {
// ignore any intermediate marks - this should take us to the first `diff` entry
while (redos.head?.type === 'stop') {
undos = undos.push(redos.head)
redos = redos.tail
}
if (redos.length === 0) {
this.ctx.emit('change-history', { reason: 'redo' })
return { undos, redos }
}
// accumulate diffs to be redone so they can be applied atomically
const diffToRedo = createEmptyRecordsDiff<R>()
while (redos.head) {
const command = redos.head
undos = undos.push(redos.head)
const redo = redos.head
undos = undos.push(redo)
redos = redos.tail
if (command.type === 'STOP') {
if (command.onRedo) {
break
}
if (redo.type === 'diff') {
squashRecordDiffsMutable(diffToRedo, [redo.diff])
} else {
const handler = this._commands[command.name]
if (handler.redo) {
handler.redo(command.data)
} else {
handler.do(command.data)
}
break
}
}
this.ctx.emit('change-history', { reason: 'redo' })
return { undos, redos }
})
this.store.applyDiff(diffToRedo, { ignoreEphemeralKeys: true })
this.store.ensureStoreIsUsable()
this.stacks.set({ undos, redos })
} finally {
this.state = previousState
}
return this
}
@ -281,24 +253,59 @@ export class HistoryManager<
return this
}
mark = (id = uniqueId(), onUndo = true, onRedo = true) => {
const mostRecent = this._undos.get().head
// dedupe marks, why not
if (mostRecent && mostRecent.type === 'STOP') {
if (mostRecent.id === id && mostRecent.onUndo === onUndo && mostRecent.onRedo === onRedo) {
return mostRecent.id
}
}
this._undos.update((undos) => undos.push({ type: 'STOP', id, onUndo, onRedo }))
this.ctx.emit('mark-history', { id })
mark = (id = uniqueId()) => {
transact(() => {
this.flushPendingDiff()
this.stacks.update(({ undos, redos }) => ({ undos: undos.push({ type: 'stop', id }), redos }))
})
return id
}
clear() {
this._undos.set(stack())
this._redos.set(stack())
this.stacks.set({ undos: stack(), redos: stack() })
this.pendingDiff.clear()
}
/** @internal */
debug() {
const { undos, redos } = this.stacks.get()
return {
undos: undos.toArray(),
redos: redos.toArray(),
pendingDiff: this.pendingDiff.debug(),
state: this.state,
}
}
}
const modeToState = {
record: HistoryRecorderState.Recording,
'record-preserveRedoStack': HistoryRecorderState.RecordingPreserveRedoStack,
ignore: HistoryRecorderState.Paused,
} as const
class PendingDiff<R extends UnknownRecord> {
private diff = createEmptyRecordsDiff<R>()
private isEmptyAtom = atom('PendingDiff.isEmpty', true)
clear() {
const diff = this.diff
this.diff = createEmptyRecordsDiff<R>()
this.isEmptyAtom.set(true)
return diff
}
isEmpty() {
return this.isEmptyAtom.get()
}
apply(diff: RecordsDiff<R>) {
squashRecordDiffsMutable(this.diff, [diff])
this.isEmptyAtom.set(isRecordsDiffEmpty(this.diff))
}
debug() {
return { diff: this.diff, isEmpty: this.isEmpty() }
}
}

Wyświetl plik

@ -88,25 +88,13 @@ export class SideEffectManager<
return next
}
let updateDepth = 0
editor.store.onAfterChange = (prev, next, source) => {
updateDepth++
if (updateDepth > 1000) {
console.error('[CleanupManager.onAfterChange] Maximum update depth exceeded, bailing out.')
} else {
const handlers = this._afterChangeHandlers[
next.typeName
] as TLAfterChangeHandler<TLRecord>[]
if (handlers) {
for (const handler of handlers) {
handler(prev, next, source)
}
const handlers = this._afterChangeHandlers[next.typeName] as TLAfterChangeHandler<TLRecord>[]
if (handlers) {
for (const handler of handlers) {
handler(prev, next, source)
}
}
updateDepth--
}
editor.store.onBeforeDelete = (record, source) => {
@ -161,6 +149,46 @@ export class SideEffectManager<
private _batchCompleteHandlers: TLBatchCompleteHandler[] = []
/**
* Internal helper for registering a bunch of side effects at once and keeping them organized.
* @internal
*/
register(handlersByType: {
[R in TLRecord as R['typeName']]?: {
beforeCreate?: TLBeforeCreateHandler<R>
afterCreate?: TLAfterCreateHandler<R>
beforeChange?: TLBeforeChangeHandler<R>
afterChange?: TLAfterChangeHandler<R>
beforeDelete?: TLBeforeDeleteHandler<R>
afterDelete?: TLAfterDeleteHandler<R>
}
}) {
const disposes: (() => void)[] = []
for (const [type, handlers] of Object.entries(handlersByType) as any) {
if (handlers?.beforeCreate) {
disposes.push(this.registerBeforeCreateHandler(type, handlers.beforeCreate))
}
if (handlers?.afterCreate) {
disposes.push(this.registerAfterCreateHandler(type, handlers.afterCreate))
}
if (handlers?.beforeChange) {
disposes.push(this.registerBeforeChangeHandler(type, handlers.beforeChange))
}
if (handlers?.afterChange) {
disposes.push(this.registerAfterChangeHandler(type, handlers.afterChange))
}
if (handlers?.beforeDelete) {
disposes.push(this.registerBeforeDeleteHandler(type, handlers.beforeDelete))
}
if (handlers?.afterDelete) {
disposes.push(this.registerAfterDeleteHandler(type, handlers.afterDelete))
}
}
return () => {
for (const dispose of disposes) dispose()
}
}
/**
* Register a handler to be called before a record of a certain type is created. Return a
* modified record from the handler to change the record that will be created.

Wyświetl plik

@ -15,8 +15,6 @@ export interface TLEventMap {
event: [TLEventInfo]
tick: [number]
frame: [number]
'change-history': [{ reason: 'undo' | 'redo' | 'push' } | { reason: 'bail'; markId?: string }]
'mark-history': [{ id: string }]
'select-all-text': [{ shapeId: TLShapeId }]
}

Wyświetl plik

@ -1,50 +1,27 @@
/** @public */
export type TLCommandHistoryOptions = Partial<{
/**
* When true, this command will be squashed with the previous command in the undo / redo stack.
*/
squashing: boolean
/**
* When true, this command will not add anything to the undo / redo stack. Its change will never be undone or redone.
*/
ephemeral: boolean
/**
* When true, adding this this command will not clear out the redo stack.
*/
preservesRedoStack: boolean
}>
import { RecordsDiff, UnknownRecord } from '@tldraw/store'
/** @public */
export type TLHistoryMark = {
type: 'STOP'
export interface TLHistoryMark {
type: 'stop'
id: string
onUndo: boolean
onRedo: boolean
}
/** @public */
export type TLCommand<Name extends string = any, Data = any> = {
type: 'command'
data: Data
name: Name
export interface TLHistoryDiff<R extends UnknownRecord> {
type: 'diff'
diff: RecordsDiff<R>
}
/** @public */
export type TLHistoryEntry<R extends UnknownRecord> = TLHistoryMark | TLHistoryDiff<R>
/** @public */
export interface TLHistoryBatchOptions {
/**
* Allows for commands that change state and should be undoable, but are 'inconsequential' and
* should not clear the redo stack. e.g. modifying the set of selected ids.
* How should this change interact with the history stack?
* - record: Add to the undo stack and clear the redo stack
* - record-preserveRedoStack: Add to the undo stack but do not clear the redo stack
* - ignore: Do not add to the undo stack or the redo stack
*/
preservesRedoStack?: boolean
}
/** @public */
export type TLHistoryEntry = TLHistoryMark | TLCommand
/** @public */
export type TLCommandHandler<Data> = {
do: (data: Data) => void
undo: (data: Data) => void
redo?: (data: Data) => void
/**
* Allow to combine the next command with the previous one if possible. Useful for, e.g. combining
* a series of shape translation commands into one command in the undo stack
*/
squash?: (prevData: Data, nextData: Data) => Data
history?: 'record' | 'record-preserveRedoStack' | 'ignore'
}

Wyświetl plik

@ -4,27 +4,68 @@ import { useEditor } from './useEditor'
/** @internal */
export function useCoarsePointer() {
const editor = useEditor()
useEffect(() => {
// We'll track our own state for the pointer type
let isCoarse = editor.getInstanceState().isCoarsePointer
// 1.
// We'll use touch events / mouse events to detect coarse pointer.
// When the user touches the screen, we assume they have a coarse pointer
const handleTouchStart = () => {
if (isCoarse) return
isCoarse = true
editor.updateInstanceState({ isCoarsePointer: true })
}
// When the user moves the mouse, we assume they have a fine pointer
const handleMouseMove = () => {
if (!isCoarse) return
isCoarse = false
editor.updateInstanceState({ isCoarsePointer: false })
}
// Set up the listeners for touch and mouse events
window.addEventListener('touchstart', handleTouchStart)
window.addEventListener('mousemove', handleMouseMove)
// 2.
// We can also use the media query to detect / set the initial pointer type
// and update the state if the pointer type changes.
// We want the touch / mouse events to run even if the browser does not
// support matchMedia. We'll have to handle the media query changes
// conditionally in the code below.
const mql = window.matchMedia && window.matchMedia('(any-pointer: coarse)')
// This is a workaround for a Firefox bug where we don't correctly
// detect coarse VS fine pointer. For now, let's assume that you have a fine
// pointer if you're on Firefox on desktop.
if (
editor.environment.isFirefox &&
!editor.environment.isAndroid &&
!editor.environment.isIos
) {
editor.updateInstanceState({ isCoarsePointer: false })
return
const isForcedFinePointer =
editor.environment.isFirefox && !editor.environment.isAndroid && !editor.environment.isIos
const handleMediaQueryChange = () => {
const next = isForcedFinePointer ? false : mql.matches // get the value from the media query
if (isCoarse !== next) return // bail if the value hasn't changed
isCoarse = next // update the local value
editor.updateInstanceState({ isCoarsePointer: next }) // update the value in state
}
if (window.matchMedia) {
const mql = window.matchMedia('(pointer: coarse)')
const handler = () => {
editor.updateInstanceState({ isCoarsePointer: !!mql.matches })
}
handler()
if (mql) {
// set up the listener
mql.addEventListener('change', handleMediaQueryChange)
// and run the handler once to set the initial value
handleMediaQueryChange()
}
return () => {
window.removeEventListener('touchstart', handleTouchStart)
window.removeEventListener('mousemove', handleMouseMove)
if (mql) {
mql.addEventListener('change', handler)
return () => mql.removeEventListener('change', handler)
mql.removeEventListener('change', handleMediaQueryChange)
}
}
}, [editor])

Wyświetl plik

@ -1,177 +0,0 @@
{
"metadata": {
"toolPackage": "@microsoft/api-extractor",
"toolVersion": "7.43.1",
"schemaVersion": 1011,
"oldestForwardsCompatibleVersion": 1001,
"tsdocConfig": {
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
"noStandardTags": true,
"tagDefinitions": [
{
"tagName": "@alpha",
"syntaxKind": "modifier"
},
{
"tagName": "@beta",
"syntaxKind": "modifier"
},
{
"tagName": "@defaultValue",
"syntaxKind": "block"
},
{
"tagName": "@decorator",
"syntaxKind": "block",
"allowMultiple": true
},
{
"tagName": "@deprecated",
"syntaxKind": "block"
},
{
"tagName": "@eventProperty",
"syntaxKind": "modifier"
},
{
"tagName": "@example",
"syntaxKind": "block",
"allowMultiple": true
},
{
"tagName": "@experimental",
"syntaxKind": "modifier"
},
{
"tagName": "@inheritDoc",
"syntaxKind": "inline"
},
{
"tagName": "@internal",
"syntaxKind": "modifier"
},
{
"tagName": "@label",
"syntaxKind": "inline"
},
{
"tagName": "@link",
"syntaxKind": "inline",
"allowMultiple": true
},
{
"tagName": "@override",
"syntaxKind": "modifier"
},
{
"tagName": "@packageDocumentation",
"syntaxKind": "modifier"
},
{
"tagName": "@param",
"syntaxKind": "block",
"allowMultiple": true
},
{
"tagName": "@privateRemarks",
"syntaxKind": "block"
},
{
"tagName": "@public",
"syntaxKind": "modifier"
},
{
"tagName": "@readonly",
"syntaxKind": "modifier"
},
{
"tagName": "@remarks",
"syntaxKind": "block"
},
{
"tagName": "@returns",
"syntaxKind": "block"
},
{
"tagName": "@sealed",
"syntaxKind": "modifier"
},
{
"tagName": "@see",
"syntaxKind": "block"
},
{
"tagName": "@throws",
"syntaxKind": "block",
"allowMultiple": true
},
{
"tagName": "@typeParam",
"syntaxKind": "block",
"allowMultiple": true
},
{
"tagName": "@virtual",
"syntaxKind": "modifier"
},
{
"tagName": "@betaDocumentation",
"syntaxKind": "modifier"
},
{
"tagName": "@internalRemarks",
"syntaxKind": "block"
},
{
"tagName": "@preapproved",
"syntaxKind": "modifier"
}
],
"supportForTags": {
"@alpha": true,
"@beta": true,
"@defaultValue": true,
"@decorator": true,
"@deprecated": true,
"@eventProperty": true,
"@example": true,
"@experimental": true,
"@inheritDoc": true,
"@internal": true,
"@label": true,
"@link": true,
"@override": true,
"@packageDocumentation": true,
"@param": true,
"@privateRemarks": true,
"@public": true,
"@readonly": true,
"@remarks": true,
"@returns": true,
"@sealed": true,
"@see": true,
"@throws": true,
"@typeParam": true,
"@virtual": true,
"@betaDocumentation": true,
"@internalRemarks": true,
"@preapproved": true
},
"reportUnsupportedHtmlElements": false
}
},
"kind": "Package",
"canonicalReference": "@tldraw/tldraw!",
"docComment": "",
"name": "@tldraw/tldraw",
"preserveMemberOrder": false,
"members": [
{
"kind": "EntryPoint",
"canonicalReference": "@tldraw/tldraw!",
"name": "",
"preserveMemberOrder": false,
"members": []
}
]
}

Wyświetl plik

@ -4,7 +4,7 @@ import { HistoryBuffer } from './HistoryBuffer'
import { maybeCaptureParent, startCapturingParents, stopCapturingParents } from './capture'
import { GLOBAL_START_EPOCH } from './constants'
import { EMPTY_ARRAY, equals, haveParentsChanged, singleton } from './helpers'
import { getGlobalEpoch, getIsReacting } from './transactions'
import { getGlobalEpoch } from './transactions'
import { Child, ComputeDiff, RESET_VALUE, Signal } from './types'
import { logComputedGetterWarning } from './warnings'
@ -189,15 +189,8 @@ class __UNSAFE__Computed<Value, Diff = unknown> implements Computed<Value, Diff>
__unsafe__getWithoutCapture(ignoreErrors?: boolean): Value {
const isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH
const globalEpoch = getGlobalEpoch()
if (
!isNew &&
(this.lastCheckedEpoch === globalEpoch ||
(this.isActivelyListening && getIsReacting() && this.lastTraversedEpoch < globalEpoch) ||
!haveParentsChanged(this))
) {
this.lastCheckedEpoch = globalEpoch
if (!isNew && (this.lastCheckedEpoch === getGlobalEpoch() || !haveParentsChanged(this))) {
this.lastCheckedEpoch = getGlobalEpoch()
if (this.error) {
if (!ignoreErrors) {
throw this.error.thrownValue

Wyświetl plik

@ -70,10 +70,6 @@ export function getGlobalEpoch() {
return inst.globalEpoch
}
export function getIsReacting() {
return inst.globalIsReacting
}
/**
* Collect all of the reactors that need to run for an atom and run them.
*

Some files were not shown because too many files have changed in this diff Show More