Merge branch 'main' into custom-ui-more-tools

pull/3141/head
Kesavaraja Krishnan 2024-04-23 10:03:48 +05:30 zatwierdzone przez GitHub
commit 10f5c6902b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
336 zmienionych plików z 119984 dodań i 109612 usunięć

Wyświetl plik

@ -19,7 +19,7 @@ body:
id: reproduction
attributes:
label: How can we reproduce the bug?
description: If you can make the bug happen again, please share the steps involved.
description: If you can make the bug happen again, please share the steps involved. You can [fork this CodeSandbox](https://codesandbox.io/p/sandbox/tldraw-example-n539u) to make a reproduction.
validations:
required: false
- type: dropdown

6
.gitignore vendored
Wyświetl plik

@ -7,6 +7,8 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.rooms
node_modules
dist
dist-cjs
@ -92,4 +94,6 @@ apps/docs/content/gen
.env*
.wrangler
/vercel.json
/vercel.json
license-report-prod.html
license-report.html

Wyświetl plik

@ -77,7 +77,17 @@ Please see our [contributing guide](https://github.com/tldraw/tldraw/blob/main/C
## Star History
<a href="https://star-history.com/#tldraw/tldraw">
<img src="https://api.star-history.com/svg?repos=tldraw/tldraw&type=Date" alt="Star History Chart" width="100%" />
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="https://api.star-history.com/svg?repos=tldraw/tldraw&type=Date&theme=dark"
/>
<source
media="(prefers-color-scheme: light)"
srcset="https://api.star-history.com/svg?repos=tldraw/tldraw&type=Date"
/>
<img src="https://api.star-history.com/svg?repos=tldraw/tldraw&type=Date" alt="Star History Chart" width="100%" />
</picture>
</a>
## Contact

Wyświetl plik

@ -8,7 +8,6 @@
- Minor version bumps are released on a regular cadence. At the time of writing that cadence is monthly. **They may contain breaking changes**. We aim to make breaking changes as minimally disruptive as possible by providing warnings several releases in advance, and by providing tooling to help you migrate your code. We recommend updating tldraw at a similar pace to our release cadence, and be sure to check the release notes.
- Patch version bumps are for bugfixes and hotfixes that can't wait for the next cadence release.
## How to publish a new major or minor release
New cadence releases are published from `main`. You trigger a release manually by running the workflow defined in `publish-new.yml`.

Plik diff jest za duży Load Diff

Plik diff jest za duży Load Diff

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

@ -1,9 +1,75 @@
---
title: Collaboration
status: published
author: steveruizok
author: ds300
date: 3/22/2023
order: 8
---
See the [tldraw-yjs example](https://github.com/tldraw/tldraw-yjs-example) for an example of how to use yjs with the `tldraw` library.
We've designed the tldraw SDK to work with any collaboration backend. Depending on which backend you choose, you will need an interface that pipes changes coming from the editor to the backend and then merge changes from the backend back to the editor.
The best way to get started is by adapting one of our examples.
### Yjs sync example
We created a [tldraw-yjs example](https://github.com/tldraw/tldraw-yjs-example) to illustrate a way of using the [yjs](https://yjs.dev) library with the tldraw SDK. If you need a "drop in solution" for prototyping multiplayer experiences with tldraw, start here.
### Sockets example
We have a [sockets example](https://github.com/tldraw/tldraw-sockets-example) that uses [PartyKit](https://www.partykit.io/) as a backend. Unlike the yjs example, this example does not use any special data structures to handle conflicts. It should be a good starting point if you needed to write your own conflict-resolution logic.
### Our own sync engine
We developed our own sync engine for use on tldraw.com based on a push/pull/rebase-style algorithm. It powers our "shared projects", such as [this one](https://tldraw.com/r). The engine's source code can be found [here](https://github.com/tldraw/tldraw/tree/main/packages/tlsync). It was designed to be hosted on Cloudflare workers with [DurableObjects](https://developers.cloudflare.com/durable-objects/).
We don't suggest using this code directly. However, like our other examples, it may serve as a good reference for your own sync engine.
## Store data
For information about how to synchronize the store with other processes, i.e. how to get data out and put data in, including from remote sources, see the (Persistence)[/docs/persistence] page.
## User presence
Tldraw has support for displaying the 'presence' of other users. Presence information consists of:
- The user's pointer position
- The user's set of selected shapes
- The user's viewport bounds (the part of the canvas they are currently viewing)
- The user's name, id, and a color to represent them
This information will usually come from two sources:
- The tldraw editor state (e.g. pointer position, selected shapes)
- The data layer of whichever app tldraw has been embedded in (e.g. user name, user id)
Tldraw is agnostic about how this data is shared among users. However, in order for tldraw to use the presence data it needs to be put into the editor's store as `instance_presence` records.
We provide a helper for constructing a reactive signal for an `instance_presence` record locally, which can then be sent to other clients somehow. It is called [createPresenceStateDerivation](?).
```ts
import { createPresenceStateDerivation, react, atom } from 'tldraw'
// First you need to create a Signal containing the basic user details: id, name, and color
const user = atom<{ id: string; color: string; name: string }>('user', {
id: myUser.id,
color: myUser.color,
name: myUser.name,
})
// if you don't have your own user data backend, you can use our localStorage-only user preferences store
// import { getUserPreferences, computed } from 'tldraw'
// const user = computed('user', getUserPreferences)
// Then, with access to your store instance, you can create a presence signal
const userPresence = createPresenceStateDerivation(user)(store)
// Then you can listen for changes to the presence signal and send them to other clients
const unsub = react('update presence', () => {
const presence = userPresence.get()
broadcastPresence(presence)
})
```
The other clients would then call `store.put([presence])` to add the presence information to their store.
Any such `instance_presence` records tldraw finds in the store that have a different user `id` than the editor's configured user id will cause the presence information to be rendered on the canvas.

Wyświetl plik

@ -50,21 +50,9 @@ editor.getSelectedShapeIds() // [myShapeId, myOtherShapeId]
Each change to the state happens within a transaction. You can batch changes into a single transaction using the [Editor#batch](?) method. It's a good idea to batch wherever possible, as this reduces the overhead for persisting or distributing those changes.
### Listening for changes
### Listening for changes, and merging changes from other sources
You can subscribe to changes using the [Store#listen](?) method on [Editor#store](?). Each time a transaction completes, the editor will call the callback with a history entry. This entry contains information about the records that were added, changed, or deleted, as well as whether the change was caused by the user or from a remote change.
```ts
editor.store.listen((entry) => {
entry // { changes, source }
})
```
### Remote changes
By default, changes to the editor's store are assumed to have come from the editor itself. You can use the [Store#mergeRemoteChanges](?) method of the editor's [Editor#store](?) to make changes in the store that will be emitted via [Store#listen](?) with the `source` property as `'remote'`.
If you're setting up some kind of multiplayer backend, you would want to send only the `'user'` changes to the server and merge the changes from the server using [Store#mergeRemoteChanges](?) (`editor.store.mergeRemoteChanges`).
For information about how to synchronize the store with other processes, i.e. how to get data out and put data in, see the (Persistence)[/docs/persistence] page.
### Undo and redo

Wyświetl plik

@ -17,7 +17,7 @@ Persistence in tldraw means storing information about the editor's state to a da
## The `"persistenceKey"` prop
Both the `<Tldraw>` or `<TldrawEditor>` components support local persitence and cross-tab synchronization via the `persistenceKey` prop. Passing a value to this prop will persist the contents of the editor locally to the browser's IndexedDb.
Both the `<Tldraw>` or `<TldrawEditor>` components support local persistence and cross-tab synchronization via the `persistenceKey` prop. Passing a value to this prop will persist the contents of the editor locally to the browser's IndexedDb.
```tsx
import { Tldraw } from 'tldraw'
@ -54,7 +54,7 @@ export default function () {
In the example above, both editors would synchronize their document locally. They would still have two independent instance states (e.g. selections) but the document would be kept in sync and persisted under the same key.
## Snapshots
## Document Snapshots
You can get a JSON snapshot of the editor's content using the [Editor#store](?)'s [Store#getSnapshot](?) method.
@ -96,7 +96,7 @@ function LoadButton() {
A [snapshot](/reference/store/StoreSnapshot) includes both the store's [serialized records](/reference/store/SerializedStore) and its [serialized schema](/reference/store/SerializedSchema), which is used for migrations.
> By default, the `getSnapshot` method returns only the editor's document data. If you want to get records from a different scope, You can pass in `session`, `document`, `presence`, or else `all` for all scopes.
> By default, the `getSnapshot` method returns only the editor's document data. If you want to get records from a different scope, you can pass in `session`, `document`, `presence`, or else `all` for all scopes.
Note that loading a snapshot does not reset the editor's in memory state or UI state. For example, loading a snapshot during a resizing operation may lead to a crash. This is because the resizing state maintains its own cache of information about which shapes it is resizing, and its possible that those shapes may no longer exist!
@ -170,3 +170,242 @@ export default function () {
```
For a good example of this pattern, see the [yjs-example](https://github.com/tldraw/tldraw-yjs-example).
## Listening for changes
You can listen for incremental updates to the document state by calling `editor.store.listen`, e.g.
```ts
const unlisten = editor.store.listen(
(update) => {
console.log('update', update)
},
{ scope: 'document', source: 'user' }
)
```
These updates contain information about which records were added, removed, and updated. See [HistoryEntry](?)
The `scope` filter can be used to listen for changes to a specific record scope, e.g. `document`, `session`, `presence`, or `all`.
The `source` filter can be used to listen for changes from a specific source, e.g. `user`, `remote`, or `all`. (See [Store#mergeRemoteChanges](?) for more information on remote changes.)
Note that these incremental updates do not include the schema version. You should make sure that you keep a record of the latest schema version for your snapshots.
You can get the schema version by calling `editor.store.schema.serialize()` and the returned value can replace the `schema` property in the snapshot next time you need to load a snapshot. The schema does not change at runtime so you only need to do this once per session.
## Handling remote changes
If you need to synchronize changes from a remote source, e.g. a multiplayer backend, you can use the `editor.store.mergeRemoteChanges` method. This will 'tag' the changes with the `source` property as `'remote'` so you can filter them out when listening for changes.
```ts
myRemoteSource.on('change', (changes) => {
editor.store.mergeRemoteChanges(() => {
changes.forEach((change) => {
// Apply the changes to the store
editor.store.put(/* ... */)
})
})
})
```
## Migrations
Tldraw uses migrations to bring data from old snapshots up to date. These run automatically when calling `editor.store.loadSnapshot`.
### Running migrations manually
If you need to run migrations on a snapshot without loading it into the store, you can call [StoreSchema#migrateStoreSnapshot](?) directly.
```ts
import { createTLSchema } from 'tldraw'
const snapshot = await getSnapshotFromSomewhere()
const migrationResult = createTLSchema().migrateStoreSnapshot(snapshot)
if (migrationResult.type === 'success') {
console.log('Migrated snapshot', migrationResult.value)
} else {
console.error('Migration failed', migrationResult.reason)
}
```
### Custom migrations
Tldraw supports a couple of ways of adding custom data types to the tldraw store:
- [Custom shape types](/docs/shapes#Custom-shapes-1)
- [`meta` properties](/docs/shapes#Meta-information) on all of our built-in record types.
You might wish to migrate your custom data types over time as you make changes to them.
To enable this, tldraw provides two ways to add custom migrations:
1. **Shape props migrations**, specifically for migrating the shape.props objects on your custom shape types.
2. **The `migrations` config option**, which is more general purpose but much less commonly needed. This will allow you to migrate any data in the store.
#### Shape props migrations
If you have a custom shape type, you can define a `migrations` property on the shape util class. Use the `createShapePropsMigrationSequence` helper to define this property.
```ts
import { createShapePropsMigrationSequence, createShapePropsMigrationIds, ShapeUtil } from 'tldraw'
// Migrations must start a 1 and be sequential integers.
const Versions = createShapePropMigrationIds('custom-shape', {
AddColor: 1,
})
class MyCustomShapeUtil extends ShapeUtil {
static type = 'custom-shape'
static migrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions.AddColor,
up(props) {
// set the default color
props.color = 'black'
},
},
],
})
// ...
}
```
#### The `migrations` config option
First create a set of migration ids.
```ts
import { createMigrationIds } from 'tldraw'
// The first argument is a unique namespace for your migration sequence.
// We recommend using a reverse domain name, e.g. we use 'com.tldraw.foo.bar'
const SEQUENCE_ID = 'com.example.my-app'
const Versions = createMigrationIds(SEQUENCE_ID, {
// Migrations must start at 1 and be sequential integers.
AddColor: 1,
})
```
Then create a migration sequence.
```ts
import { createMigrationSequence, isShape } from 'tldraw'
const myMigrations = createMigrationSequence({
sequenceId: SEQUENCE_ID,
sequence: [
{
id: Versions.AddColor,
// Scope can be one of
// - 'store' to have the up function called on the whole snapshot at once
// - 'record' to have the up function called on each record individually
scope: 'record',
// if scope is 'record', you can filter which records the migration runs on
filter: (record) => isShape(record) && record.type === 'custom-shape',
up(record) {
record.props.color = 'black'
},
},
],
})
```
And finally pass your migrations in to tldraw via the `migrations` config option. There are a few places where you might need to do this, depending on how specialized your usage of Tldraw is:
```tsx
// When rendering the Tldraw component
<Tldraw
...
migrations={[myMigrations]}
/>
// or when creating the store
store = createTLStore({
...
migrations: [myMigrations],
})
// or when creating the schema
schema = createTLSchema({
...
migrations: [myMigrations],
})
```
### Updating legacy shape migrations (defineMigrations)
You can convert your legacy migrations to the new migrations format by the following process:
1. Wrap your version numbers in `createShapePropsMigrationIds`
```diff
- const Versions = {
+ const Versions = createShapePropMigrationIds('custom-shape', {
AddColor: 1
- }
+ })
```
2. Replace your `defineMigrations` call with `createShapePropsMigrationSequence`
```ts
const migrations = defineMigrations({
currentVersion: Versions.AddColor,
migrators: {
[Versions.AddColor]: {
up: (shape: any) => ({ ...shape, props: { ...shape.props, color: 'black' } }),
down: ({ props: { color, ...props }, ...shape }: any) => ({ ...shape, props }),
},
},
})
```
Becomes
```ts
const migrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions.AddColor,
// [!!!] You no longer have access to the top-level shape object.
// Only the shape.props object is passed in to the migrator function.
up(props) {
// [!!!] You no longer need to return a new copy of the shape object.
// Instead, you can modify the props object in place.
props.color = 'black'
},
// [!!!] You no longer need to specify a down migration.
},
],
})
```
## Examples
### Local persistence
Tldraw ships with a local-only sync engine based on `IndexedDb` and `BroadcastChannel` called [`TLLocalSyncClient`](https://github.com/tldraw/tldraw/blob/main/packages/editor/src/lib/utils/sync/TLLocalSyncClient.ts).
### Tldraw.com sync engine
[tldraw.com/r](https://tldraw.com/r) currently uses a simple custom sync engine based on a push/pull/rebase-style algorithm.
It can be found [here](https://github.com/tldraw/tldraw/tree/main/packages/tlsync).
It was optimized for Cloudflare workers with [DurableObjects](https://developers.cloudflare.com/durable-objects/)
We don't suggest using our code directly yet, but it may serve as a good reference for your own sync engine.
### Yjs sync example
We created a [tldraw-yjs example](https://github.com/tldraw/tldraw-yjs-example) to illustrate a way of using yjs with the tldraw SDK.
### Shape props migrations example
Our [custom-config example](/examples/shapes/tools/custom-config) shows how to add custom shape props migrations to the tldraw store.
### Meta properties migrations example
Our [custom-config example](/examples/shapes/tools/custom-config) shows how to add custom migrations to the tldraw store.

Wyświetl plik

@ -239,4 +239,6 @@ You can turn on `pointer-events` to allow users to interact inside of the shape.
You can make shapes "editable" to help decide when they're interactive or not.
...and more!
### Migrations
You can add migrations for your shape props by adding a `migrations` property to your shape's util class. See [the persistence docs](/docs/persistence#Shape-props-migrations) for more information.

Wyświetl plik

@ -31,7 +31,7 @@ To import fonts and CSS for tldraw:
- Copy and paste this into the file:
```CSS
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@500;700;&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap");
@import url("tldraw/tldraw.css");
body {

Wyświetl plik

@ -26,7 +26,7 @@
"@tldraw/tlsync": "workspace:*",
"@tldraw/utils": "workspace:*",
"@vercel/analytics": "^1.1.1",
"browser-fs-access": "^0.33.0",
"browser-fs-access": "^0.35.0",
"idb": "^7.1.1",
"nanoid": "4.0.2",
"qrcode": "^1.5.1",

Wyświetl plik

@ -304,7 +304,10 @@ const DocumentNameEditor = track(function DocumentNameEditor({
) : (
<div
className="tlui-document-name__text"
onDoubleClick={() => setState((prev) => ({ ...prev, isEditing: true }))}
onDoubleClick={() => {
editor.setEditingShape(null)
setState((prev) => ({ ...prev, isEditing: true }))
}}
>
{addRealSpaceForWhitespace(name)}
</div>

Wyświetl plik

@ -1,5 +1,6 @@
import { ReactNode, useEffect, useState, version } from 'react'
import { ReactNode, useEffect, useState } from 'react'
import { LoadingScreen } from 'tldraw'
import { version } from '../../version'
import { useUrl } from '../hooks/useUrl'
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
@ -113,7 +114,7 @@ export function IFrameProtector({
<div className="tldraw__editor tl-container">
<div className="iframe-warning__container">
<a className="iframe-warning__link" href={url} target="_blank">
{'Visit this page on tldraw.com '}
{'Visit this page on tldraw.com'}
<svg
width="15"
height="15"

Wyświetl plik

@ -6,9 +6,7 @@ export function CursorChatMenuItem() {
const actions = useActions()
const shouldShow = useValue(
'show cursor chat',
() => {
return editor.getInstanceState().isCoarsePointer && !editor.getSelectedShapes().length
},
() => editor.getCurrentToolId() === 'select' && !editor.getInstanceState().isCoarsePointer,
[editor]
)

Wyświetl plik

@ -6,13 +6,6 @@ const RELEASE_INFO = `${env} ${process.env.NEXT_PUBLIC_TLDRAW_RELEASE_INFO ?? 'u
export function DebugMenuItems() {
return (
<TldrawUiMenuGroup id="release">
<TldrawUiMenuItem
id="release-info"
label={`Version ${RELEASE_INFO}`}
onSelect={() => {
window.alert(`${RELEASE_INFO}`)
}}
/>
<TldrawUiMenuItem
id="v1"
label="Test v1 content"
@ -22,6 +15,13 @@ export function DebugMenuItems() {
window.location.reload()
}}
/>
<TldrawUiMenuItem
id="release-info"
label={'Release info'}
onSelect={() => {
window.alert(`${RELEASE_INFO}`)
}}
/>
</TldrawUiMenuGroup>
)
}

Wyświetl plik

@ -140,7 +140,7 @@ describe(ClientWebSocketAdapter, () => {
const message: TLSocketClientSentEvent<TLRecord> = {
type: 'connect',
connectRequestId: 'test',
schema: { schemaVersion: 0, storeVersion: 0, recordVersions: {} },
schema: { schemaVersion: 1, storeVersion: 0, recordVersions: {} },
protocolVersion: TLSYNC_PROTOCOL_VERSION,
lastServerClock: 0,
}

Wyświetl plik

@ -41,9 +41,9 @@ export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) {
await page.mouse.click(200, 250)
await page.keyboard.press('r')
await page.mouse.click(250, 300)
// deselect everything
await page.evaluate(() => editor.selectNone())
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
}
export async function cleanupPage(page: PlaywrightTestArgs['page']) {

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 5.2 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.9 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 5.2 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.9 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 5.2 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 5.0 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 5.2 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 5.0 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 4.3 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.2 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 4.3 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.2 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 4.4 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 4.4 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.6 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.6 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.1 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.1 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.9 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.9 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.8 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.8 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.4 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.8 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.4 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.8 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.6 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.6 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.6 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.6 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.5 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.5 KiB

Wyświetl plik

@ -21,6 +21,7 @@ test.describe('Canvas events', () => {
await page.mouse.move(200, 200) // to kill any double clicks
await page.mouse.move(100, 100)
await page.mouse.down()
await page.waitForTimeout(20)
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
target: 'canvas',
type: 'pointer',
@ -46,6 +47,7 @@ test.describe('Canvas events', () => {
await page.mouse.down()
await page.mouse.move(101, 101)
await page.mouse.up()
await page.waitForTimeout(20)
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
target: 'canvas',
type: 'pointer',
@ -118,6 +120,7 @@ test.describe('Shape events', () => {
test('pointer down', async () => {
await page.mouse.move(51, 51)
await page.mouse.down()
await page.waitForTimeout(20)
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
target: 'canvas',
type: 'pointer',
@ -128,6 +131,7 @@ test.describe('Shape events', () => {
test('pointer move', async () => {
await page.mouse.move(51, 51)
await page.mouse.move(52, 52)
await page.waitForTimeout(20)
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
target: 'canvas',
type: 'pointer',
@ -139,6 +143,7 @@ test.describe('Shape events', () => {
await page.mouse.move(51, 51)
await page.mouse.down()
await page.mouse.up()
await page.waitForTimeout(20)
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
target: 'canvas',
type: 'pointer',

Wyświetl plik

@ -112,6 +112,7 @@ test.describe('Shape Tools', () => {
// Click on the page
await page.mouse.click(200, 200)
await page.waitForTimeout(20)
// We should have a corresponding shape in the page
expect(await getAllShapeTypes(page)).toEqual([shape])
@ -119,6 +120,7 @@ test.describe('Shape Tools', () => {
// Reset for next time
await page.mouse.click(50, 50) // to ensure we're not focused
await page.keyboard.press('v') // go to the select tool
await page.waitForTimeout(20)
await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace')
}
@ -156,6 +158,7 @@ test.describe('Shape Tools', () => {
// Reset for next time
await page.mouse.click(50, 50) // to ensure we're not focused
await page.keyboard.press('v')
await page.waitForTimeout(20)
await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace')
}

Wyświetl plik

@ -1,5 +1,5 @@
import test, { Page, expect } from '@playwright/test'
import { BoxModel, Editor } from 'tldraw'
import { BoxModel, Editor, TLNoteShape, TLShapeId } from 'tldraw'
import { setupPage } from '../shared-e2e'
export function sleep(ms: number) {
@ -242,4 +242,92 @@ test.describe('text measurement', () => {
expect(formatLines(spans)).toEqual([[' \n'], [' \n'], [' \n'], [' ']])
})
test('for auto-font-sizing shapes, should do normal font size for text that does not have long words', async () => {
const shape = await page.evaluate(() => {
const id = 'shape:testShape' as TLShapeId
editor.createShapes([
{
id,
type: 'note',
x: 0,
y: 0,
props: {
text: 'this is just some regular text',
size: 'xl',
},
},
])
return editor.getShape(id) as TLNoteShape
})
expect(shape.props.fontSizeAdjustment).toEqual(32)
})
test('for auto-font-sizing shapes, should auto-size text that have slightly long words', async () => {
const shape = await page.evaluate(() => {
const id = 'shape:testShape' as TLShapeId
editor.createShapes([
{
id,
type: 'note',
x: 0,
y: 0,
props: {
text: 'Amsterdam',
size: 'xl',
},
},
])
return editor.getShape(id) as TLNoteShape
})
expect(shape.props.fontSizeAdjustment).toEqual(27)
})
test('for auto-font-sizing shapes, should auto-size text that have long words', async () => {
const shape = await page.evaluate(() => {
const id = 'shape:testShape' as TLShapeId
editor.createShapes([
{
id,
type: 'note',
x: 0,
y: 0,
props: {
text: 'this is a tentoonstelling',
size: 'xl',
},
},
])
return editor.getShape(id) as TLNoteShape
})
expect(shape.props.fontSizeAdjustment).toEqual(20)
})
test('for auto-font-sizing shapes, should wrap text that has words that are way too long', async () => {
const shape = await page.evaluate(() => {
const id = 'shape:testShape' as TLShapeId
editor.createShapes([
{
id,
type: 'note',
x: 0,
y: 0,
props: {
text: 'a very long dutch word like ziekenhuisinrichtingsmaatschappij',
size: 'xl',
},
},
])
return editor.getShape(id) as TLNoteShape
})
expect(shape.props.fontSizeAdjustment).toEqual(14)
})
})

Wyświetl plik

@ -0,0 +1,27 @@
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function BasicExample() {
return (
<div className="tldraw__editor">
<Tldraw
overrides={{
actions: (_editor, actions, _helpers) => {
const newActions = {
...actions,
delete: { ...actions['delete'], kbd: 'x' },
}
return newActions
},
}}
/>
</div>
)
}
/*
This example shows how you can override tldraw's actions object to change the keyboard shortcuts.
In this case we're changing the delete action's shortcut to 'x'. To customize the actions menu
please see the custom actions menu example. For more information on keyboard shortcuts see the
keyboard shortcuts example.
*/

Wyświetl plik

@ -0,0 +1,12 @@
---
title: Action overrides
component: ./ActionOverridesExample.tsx
category: ui
priority: 2
---
Override tldraw's actions
---
This example shows how you can override tldraw's actions object to change the keyboard shortcuts. In this case we're changing the delete action's shortcut to 'x'. To customize the actions menu please see the custom actions menu example. For more information on keyboard shortcuts see the keyboard shortcuts example.

Wyświetl plik

@ -1,4 +1,4 @@
import { Editor, Tldraw } from 'tldraw'
import { Editor, TLStoreSnapshot, Tldraw } from 'tldraw'
import { PlayingCardTool } from './PlayingCardShape/playing-card-tool'
import { PlayingCardUtil } from './PlayingCardShape/playing-card-util'
import snapshot from './snapshot.json'
@ -27,7 +27,7 @@ export default function BoundsSnappingShapeExample() {
// [c]
onMount={handleMount}
// [d]
snapshot={snapshot}
snapshot={snapshot as TLStoreSnapshot}
/>
</div>
)

Wyświetl plik

@ -1,21 +1,26 @@
import { defineMigrations } from 'tldraw'
import { createShapePropsMigrationIds } from '@tldraw/tlschema/src/records/TLShape'
import { createShapePropsMigrationSequence } from 'tldraw'
const versions = createShapePropsMigrationIds(
// this must match the shape type in the shape definition
'card',
{
AddSomeProperty: 1,
}
)
// Migrations for the custom card shape (optional but very helpful)
export const cardShapeMigrations = defineMigrations({
currentVersion: 1,
migrators: {
1: {
// for example, removing a property from the shape
up(shape) {
const migratedUpShape = { ...shape }
delete migratedUpShape._somePropertyToRemove
return migratedUpShape
export const cardShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: versions.AddSomeProperty,
up(props) {
// it is safe to mutate the props object here
props.someProperty = 'some value'
},
down(shape) {
const migratedDownShape = { ...shape }
migratedDownShape._somePropertyToRemove = 'some value'
return migratedDownShape
down(props) {
delete props.someProperty
},
},
},
],
})

Wyświetl plik

@ -36,7 +36,8 @@ export function CustomRenderer() {
const theme = getDefaultColorTheme({ isDarkMode: editor.user.getIsDarkMode() })
const currentPageId = editor.getCurrentPageId()
for (const { shape, maskedPageBounds, opacity } of renderingShapes) {
for (const { shape, opacity } of renderingShapes) {
const maskedPageBounds = editor.getShapeMaskedPageBounds(shape)
if (!maskedPageBounds) continue
ctx.save()

Wyświetl plik

@ -0,0 +1,151 @@
import {
Circle2d,
Geometry2d,
HTMLContainer,
Rectangle2d,
ShapeUtil,
TLBaseShape,
TLShape,
Tldraw,
} from 'tldraw'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this file!
// [1]
type MyGridShape = TLBaseShape<'my-grid-shape', Record<string, never>>
type MyCounterShape = TLBaseShape<'my-counter-shape', Record<string, never>>
// [2]
const SLOT_SIZE = 100
class MyCounterShapeUtil extends ShapeUtil<MyCounterShape> {
static override type = 'my-counter-shape' as const
override canResize = () => false
override hideResizeHandles = () => true
getDefaultProps(): MyCounterShape['props'] {
return {}
}
getGeometry(): Geometry2d {
return new Circle2d({ radius: SLOT_SIZE / 2 - 10, isFilled: true })
}
component() {
return (
<HTMLContainer
style={{
backgroundColor: '#e03131',
border: '1px solid #ff8787',
borderRadius: '50%',
}}
/>
)
}
indicator() {
return <circle r={SLOT_SIZE / 2 - 10} cx={SLOT_SIZE / 2 - 10} cy={SLOT_SIZE / 2 - 10} />
}
}
// [3]
class MyGridShapeUtil extends ShapeUtil<MyGridShape> {
static override type = 'my-grid-shape' as const
getDefaultProps(): MyGridShape['props'] {
return {}
}
getGeometry(): Geometry2d {
return new Rectangle2d({
width: SLOT_SIZE * 5,
height: SLOT_SIZE * 2,
isFilled: true,
})
}
override canResize = () => false
override hideResizeHandles = () => true
// [a]
override canDropShapes = (shape: MyGridShape, shapes: TLShape[]) => {
if (shapes.every((s) => s.type === 'my-counter-shape')) {
return true
}
return false
}
// [b]
override onDragShapesOver = (shape: MyGridShape, shapes: TLShape[]) => {
if (!shapes.every((child) => child.parentId === shape.id)) {
this.editor.reparentShapes(shapes, shape.id)
}
}
// [c]
override onDragShapesOut = (shape: MyGridShape, shapes: TLShape[]) => {
this.editor.reparentShapes(shapes, this.editor.getCurrentPageId())
}
component() {
return (
<HTMLContainer
style={{
backgroundColor: '#efefef',
borderRight: '1px solid #ccc',
borderBottom: '1px solid #ccc',
backgroundSize: `${SLOT_SIZE}px ${SLOT_SIZE}px`,
backgroundImage: `
linear-gradient(to right, #ccc 1px, transparent 1px),
linear-gradient(to bottom, #ccc 1px, transparent 1px)
`,
}}
/>
)
}
indicator() {
return <rect width={SLOT_SIZE * 5} height={SLOT_SIZE * 2} />
}
}
export default function DragAndDropExample() {
return (
<div className="tldraw__editor">
<Tldraw
shapeUtils={[MyGridShapeUtil, MyCounterShapeUtil]}
onMount={(editor) => {
editor.createShape({ type: 'my-grid-shape', x: 100, y: 100 })
editor.createShape({ type: 'my-counter-shape', x: 700, y: 100 })
editor.createShape({ type: 'my-counter-shape', x: 750, y: 200 })
editor.createShape({ type: 'my-counter-shape', x: 770, y: 300 })
}}
/>
</div>
)
}
/*
This example demonstrates how to use the drag-and-drop system.
[1] Define some shape types. For the purposes of this example, we'll define two
shapes: a grid and a counter.
[2] Make a shape util for the first shape. For this example, we'll make a simple
red circle that you drag and drop onto the other shape.
[3] Make the other shape util. In this example, we'll make a grid that you can
place the the circle counters onto.
[a] Use the `canDropShapes` method to specify which shapes can be dropped onto
the grid shape.
[b] Use the `onDragShapesOver` method to reparent counters to the grid shape
when they are dragged on top.
[c] Use the `onDragShapesOut` method to reparent counters back to the page
when they are dragged off.
*/

Wyświetl plik

@ -0,0 +1,12 @@
---
title: Drag and drop
component: ./DragAndDropExample.tsx
category: shapes/tools
priority: 1
---
Shapes that can be dragged and dropped onto each other.
---
You can create custom shapes that can be dragged and dropped onto each other.

Wyświetl plik

@ -1,9 +1,10 @@
import {
ContextMenu,
DefaultContextMenuContent,
ErrorScreen,
LoadingScreen,
TldrawEditor,
TldrawHandles,
TldrawHoveredShapeIndicator,
TldrawScribble,
TldrawSelectionBackground,
TldrawSelectionForeground,
@ -11,7 +12,9 @@ import {
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!
@ -23,11 +26,20 @@ const defaultComponents = {
SelectionForeground: TldrawSelectionForeground,
SelectionBackground: TldrawSelectionBackground,
Handles: TldrawHandles,
HoveredShapeIndicator: TldrawHoveredShapeIndicator,
}
//[2]
export default function ExplodedExample() {
const assetLoading = usePreloadAssets(defaultEditorAssetUrls)
if (assetLoading.error) {
return <ErrorScreen>Could not load assets.</ErrorScreen>
}
if (!assetLoading.done) {
return <LoadingScreen>Loading assets...</LoadingScreen>
}
return (
<div className="tldraw__editor">
<TldrawEditor

Wyświetl plik

@ -1,5 +1,14 @@
import { useState } from 'react'
import { Box, Editor, StoreSnapshot, TLPageId, TLRecord, Tldraw, TldrawImage } from 'tldraw'
import {
Box,
Editor,
StoreSnapshot,
TLPageId,
TLRecord,
TLStoreSnapshot,
Tldraw,
TldrawImage,
} from 'tldraw'
import 'tldraw/tldraw.css'
import initialSnapshot from './snapshot.json'
@ -7,7 +16,9 @@ import initialSnapshot from './snapshot.json'
export default function TldrawImageExample() {
const [editor, setEditor] = useState<Editor>()
const [snapshot, setSnapshot] = useState<StoreSnapshot<TLRecord>>(initialSnapshot)
const [snapshot, setSnapshot] = useState<StoreSnapshot<TLRecord>>(
initialSnapshot as TLStoreSnapshot
)
const [currentPageId, setCurrentPageId] = useState<TLPageId | undefined>()
const [showBackground, setShowBackground] = useState(true)
const [isDarkMode, setIsDarkMode] = useState(false)

Wyświetl plik

@ -1,6 +1,5 @@
import { TLUiActionsContextType, TLUiOverrides, TLUiToolsContextType, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
import jsonSnapshot from './snapshot.json'
// There's a guide at the bottom of this file!
@ -8,13 +7,18 @@ import jsonSnapshot from './snapshot.json'
const overrides: TLUiOverrides = {
//[a]
actions(_editor, actions): TLUiActionsContextType {
actions['toggle-grid'].kbd = 'x'
return actions
const newActions = {
...actions,
'toggle-grid': { ...actions['toggle-grid'], kbd: 'x' },
'copy-as-png': { ...actions['copy-as-png'], kbd: '$1' },
}
return newActions
},
//[b]
tools(_editor, tools): TLUiToolsContextType {
tools['draw'].kbd = 'p'
return tools
const newTools = { ...tools, draw: { ...tools.draw, kbd: 'p' } }
return newTools
},
}
@ -22,7 +26,7 @@ const overrides: TLUiOverrides = {
export default function KeyboardShortcuts() {
return (
<div className="tldraw__editor">
<Tldraw persistenceKey="tldraw_kbd_shortcuts" overrides={overrides} snapshot={jsonSnapshot} />
<Tldraw overrides={overrides} />
</div>
)
}

Wyświetl plik

@ -5,8 +5,14 @@ category: ui
priority: 2
---
Override default keyboard shortcuts.
How to replace tldraw's default keyboard shortcuts with your own.
---
How to replace tldraw's default keyboard shortcuts with your own.
This example shows how you can replace tldraw's default keyboard shortcuts with your own,
or add a shortcut for an action that doesn't have one. An example of how to add shortcuts
for custom tools can be found in the custom-config example.
- Toggle show grid by pressing 'x'
- Select the Draw tool by pressing 'p'
- Copy as png by pressing 'ctrl/cmd + 1'

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -0,0 +1,74 @@
import { Tldraw, createMigrationIds, createMigrationSequence } from 'tldraw'
import 'tldraw/tldraw.css'
import { snapshot } from './snapshot'
import { components } from './ui-overrides'
/**
* This example demonstrates how to add custom migrations for `meta` properties, or any other
* data in your store snapshots.
*
* If you have a custom shape type and you want to add migrations for its `props` object,
* there is a simpler dedicated API for that. Check out [the docs](https://tldraw.dev/docs/persistence#Shape-props-migrations) for more info.
*/
/**
* Let's say you added some page metadata, e.g. to allow setting the background color of a page independently.
*/
interface _PageMetaV1 {
backgroundTheme?: 'red' | 'blue' | 'green' | 'purple'
}
/**
* And then perhaps later on you decided to remove support for 'purple' because it's an ugly color.
* So all purple pages will become blue.
*/
export interface PageMetaV2 {
backgroundTheme?: 'red' | 'blue' | 'green'
}
/**
* You would then create a migration to update the metadata from v1 to v2.
*/
// First pick a 'sequence id' that is unique to your app
const sequenceId = 'com.example.my-app'
// Then create a 'migration id' for each version of your metadata
const versions = createMigrationIds(sequenceId, {
// the numbers must start at 1 and increment by 1
RemovePurple: 1,
})
const migrations = createMigrationSequence({
sequenceId,
sequence: [
{
id: versions.RemovePurple,
// `scope: 'record` tells the schema to call this migration on individual records.
// `scope: 'store'` would call it on the entire snapshot, to allow for actions like deleting/creating records.
scope: 'record',
// When `scope` is 'record', you can specify a filter function to only apply the migration to records that match the filter.
filter: (record) => record.typeName === 'page',
// This up function will be called on all records that match the filter
up(page: any) {
if (page.meta.backgroundTheme === 'purple') {
page.meta.backgroundTheme = 'blue'
page.name += ' (was purple)'
}
},
},
],
})
export default function MetaMigrationsExample() {
return (
<div className="tldraw__editor">
<Tldraw
// Pass in the custom migrations
migrations={[migrations]}
// When you load a snapshot from a previous version, the migrations will be applied automatically
snapshot={snapshot}
// This adds a dropdown to the canvas for changing the backgroundTheme property
components={components}
/>
</div>
)
}

Wyświetl plik

@ -0,0 +1,12 @@
---
title: Meta Migrations
component: ./MetaMigrations.tsx
category: data/assets
priority: 6
---
Create custom migrations for `meta` properties.
---
You can add arbitrary data migrations for tldraw snapshot data. This is mainly useful for updating the `meta` property of a record as your data types evolve.

Wyświetl plik

@ -0,0 +1,57 @@
import { TLStoreSnapshot } from 'tldraw'
export const snapshot = {
store: {
'document:document': {
gridSize: 10,
name: '',
meta: {},
id: 'document:document',
typeName: 'document',
},
'page:red': {
meta: {
backgroundTheme: 'red',
},
id: 'page:red',
name: 'Red',
index: 'a1',
typeName: 'page',
},
'page:green': {
meta: {
backgroundTheme: 'green',
},
id: 'page:green',
name: 'Green',
index: 'a2',
typeName: 'page',
},
'page:blue': {
meta: {
backgroundTheme: 'blue',
},
id: 'page:blue',
name: 'Blue',
index: 'a3',
typeName: 'page',
},
'page:purple': {
meta: {
backgroundTheme: 'purple',
},
id: 'page:purple',
name: 'Purple',
index: 'a0',
typeName: 'page',
},
},
schema: {
schemaVersion: 2,
sequences: {
'com.tldraw.store': 4,
'com.tldraw.document': 2,
'com.tldraw.page': 1,
},
},
} as TLStoreSnapshot

Wyświetl plik

@ -0,0 +1,41 @@
import { useLayoutEffect } from 'react'
import { TLComponents, track, useEditor } from 'tldraw'
import { PageMetaV2 } from './MetaMigrations'
export const components: TLComponents = {
TopPanel: track(() => {
const editor = useEditor()
const currentPage = editor.getCurrentPage()
const meta: PageMetaV2 = currentPage.meta
useLayoutEffect(() => {
const elem = document.querySelector('.tl-background') as HTMLElement
if (!elem) return
elem.style.backgroundColor = meta.backgroundTheme ?? 'unset'
}, [meta.backgroundTheme])
return (
<span style={{ pointerEvents: 'all', padding: '5px 15px', margin: 10, fontSize: 18 }}>
bg: &nbsp;
<select
value={meta.backgroundTheme ?? 'none'}
onChange={(e) => {
if (e.currentTarget.value === 'none') {
editor.updatePage({ ...currentPage, meta: {} })
} else {
editor.updatePage({
...currentPage,
meta: { backgroundTheme: e.currentTarget.value },
})
}
}}
>
<option value="none">None</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
</span>
)
}),
}

Wyświetl plik

@ -84,7 +84,7 @@ deletes that shape.
[1]
This is where we define our state node by extending the StateNode class. Since
there are no children states We can simply give it an id and define methods we
there are no children states We can give it an id and define methods we
want to override to handle events.

Wyświetl plik

@ -5,26 +5,34 @@ export function useChangedShapesReactor(
cb: (info: { culled: TLShape[]; restored: TLShape[] }) => void
) {
const editor = useEditor()
const rPrevShapes = useRef(editor.getRenderingShapes())
const rPrevShapes = useRef({
renderingShapes: editor.getRenderingShapes(),
culledShapes: editor.getCulledShapes(),
})
useEffect(() => {
return react('when rendering shapes change', () => {
const after = editor.getRenderingShapes()
const after = {
culledShapes: editor.getCulledShapes(),
renderingShapes: editor.getRenderingShapes(),
}
const before = rPrevShapes.current
const culled: TLShape[] = []
const restored: TLShape[] = []
const beforeToVisit = new Set(before)
const beforeToVisit = new Set(before.renderingShapes)
for (const afterInfo of after) {
const beforeInfo = before.find((s) => s.id === afterInfo.id)
for (const afterInfo of after.renderingShapes) {
const beforeInfo = before.renderingShapes.find((s) => s.id === afterInfo.id)
if (!beforeInfo) {
continue
} else {
if (afterInfo.isCulled && !beforeInfo.isCulled) {
const isAfterCulled = after.culledShapes.has(afterInfo.id)
const isBeforeCulled = before.culledShapes.has(beforeInfo.id)
if (isAfterCulled && !isBeforeCulled) {
culled.push(afterInfo.shape)
} else if (!afterInfo.isCulled && beforeInfo.isCulled) {
} else if (!isAfterCulled && isBeforeCulled) {
restored.push(afterInfo.shape)
}
beforeToVisit.delete(beforeInfo)

Wyświetl plik

@ -4,6 +4,7 @@ import {
T,
TLBaseShape,
TLOnResizeHandler,
TLStoreSnapshot,
Tldraw,
resizeBox,
} from 'tldraw'
@ -94,7 +95,7 @@ export default function ShapeWithMigrationsExample() {
// Pass in the array of custom shape classes
shapeUtils={customShapeUtils}
// Use a snapshot to load an old version of the shape
snapshot={snapshot}
snapshot={snapshot as TLStoreSnapshot}
/>
</div>
)

Wyświetl plik

@ -0,0 +1,12 @@
---
title: Slideshow
component: ./SlidesExample.tsx
category: use-cases
priority: 1
---
Slideshow example.
---
Make slides for a presentation.

Wyświetl plik

@ -0,0 +1,7 @@
import { BaseBoxShapeTool } from 'tldraw'
export class SlideShapeTool extends BaseBoxShapeTool {
static override id = 'slide'
static override initial = 'idle'
override shapeType = 'slide'
}

Wyświetl plik

@ -0,0 +1,127 @@
import { useCallback } from 'react'
import {
Geometry2d,
Rectangle2d,
SVGContainer,
ShapeProps,
ShapeUtil,
T,
TLBaseShape,
TLOnResizeHandler,
resizeBox,
useValue,
} from 'tldraw'
import { getPerfectDashProps } from 'tldraw/src/lib/shapes/shared/getPerfectDashProps'
import { moveToSlide, useSlides } from './useSlides'
export type SlideShape = TLBaseShape<
'slide',
{
w: number
h: number
}
>
export class SlideShapeUtil extends ShapeUtil<SlideShape> {
static override type = 'slide' as const
static override props: ShapeProps<SlideShape> = {
w: T.number,
h: T.number,
}
override canBind = () => false
override hideRotateHandle = () => true
getDefaultProps(): SlideShape['props'] {
return {
w: 720,
h: 480,
}
}
getGeometry(shape: SlideShape): Geometry2d {
return new Rectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: false,
})
}
override onRotate = (initial: SlideShape) => initial
override onResize: TLOnResizeHandler<SlideShape> = (shape, info) => {
return resizeBox(shape, info)
}
override onDoubleClick = (shape: SlideShape) => {
moveToSlide(this.editor, shape)
this.editor.selectNone()
}
override onDoubleClickEdge = (shape: SlideShape) => {
moveToSlide(this.editor, shape)
this.editor.selectNone()
}
component(shape: SlideShape) {
const bounds = this.editor.getShapeGeometry(shape).bounds
// eslint-disable-next-line react-hooks/rules-of-hooks
const zoomLevel = useValue('zoom level', () => this.editor.getZoomLevel(), [this.editor])
// eslint-disable-next-line react-hooks/rules-of-hooks
const slides = useSlides()
const index = slides.findIndex((s) => s.id === shape.id)
// eslint-disable-next-line react-hooks/rules-of-hooks
const handleLabelPointerDown = useCallback(() => this.editor.select(shape.id), [shape.id])
if (!bounds) return null
return (
<>
<div onPointerDown={handleLabelPointerDown} className="slide-shape-label">
{`Slide ${index + 1}`}
</div>
<SVGContainer>
<g
style={{
stroke: 'var(--color-text)',
strokeWidth: 'calc(1px * var(--tl-scale))',
opacity: 0.25,
}}
pointerEvents="none"
strokeLinecap="round"
strokeLinejoin="round"
>
{bounds.sides.map((side, i) => {
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
side[0].dist(side[1]),
1 / zoomLevel,
{
style: 'dashed',
lengthRatio: 6,
}
)
return (
<line
key={i}
x1={side[0].x}
y1={side[0].y}
x2={side[1].x}
y2={side[1].y}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
/>
)
})}
</g>
</SVGContainer>
</>
)
}
indicator(shape: SlideShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
}

Wyświetl plik

@ -0,0 +1,109 @@
import {
DefaultKeyboardShortcutsDialog,
DefaultKeyboardShortcutsDialogContent,
DefaultToolbar,
DefaultToolbarContent,
TLComponents,
TLUiOverrides,
Tldraw,
TldrawUiMenuItem,
computed,
track,
useIsToolSelected,
useTools,
} from 'tldraw'
import 'tldraw/tldraw.css'
import { SlideShapeTool } from './SlideShapeTool'
import { SlideShapeUtil } from './SlideShapeUtil'
import { SlidesPanel } from './SlidesPanel'
import './slides.css'
import { $currentSlide, getSlides, moveToSlide } from './useSlides'
const components: TLComponents = {
HelperButtons: SlidesPanel,
Minimap: null,
Toolbar: (props) => {
const tools = useTools()
const isSlideSelected = useIsToolSelected(tools['slide'])
return (
<DefaultToolbar {...props}>
<TldrawUiMenuItem {...tools['slide']} isSelected={isSlideSelected} />
<DefaultToolbarContent />
</DefaultToolbar>
)
},
KeyboardShortcutsDialog: (props) => {
const tools = useTools()
return (
<DefaultKeyboardShortcutsDialog {...props}>
<TldrawUiMenuItem {...tools['slide']} />
<DefaultKeyboardShortcutsDialogContent />
</DefaultKeyboardShortcutsDialog>
)
},
}
const overrides: TLUiOverrides = {
actions(editor, actions) {
const $slides = computed('slides', () => getSlides(editor))
return {
...actions,
'next-slide': {
id: 'next-slide',
label: 'Next slide',
kbd: 'right',
onSelect() {
const slides = $slides.get()
const currentSlide = $currentSlide.get()
const index = slides.findIndex((s) => s.id === currentSlide?.id)
const nextSlide = slides[index + 1] ?? currentSlide ?? slides[0]
if (nextSlide) {
editor.stopCameraAnimation()
moveToSlide(editor, nextSlide)
}
},
},
'previous-slide': {
id: 'previous-slide',
label: 'Previous slide',
kbd: 'left',
onSelect() {
const slides = $slides.get()
const currentSlide = $currentSlide.get()
const index = slides.findIndex((s) => s.id === currentSlide?.id)
const previousSlide = slides[index - 1] ?? currentSlide ?? slides[slides.length - 1]
if (previousSlide) {
editor.stopCameraAnimation()
moveToSlide(editor, previousSlide)
}
},
},
}
},
tools(editor, tools) {
tools.slide = {
id: 'slide',
icon: 'group',
label: 'Slide',
kbd: 's',
onSelect: () => editor.setCurrentTool('slide'),
}
return tools
},
}
const SlidesExample = track(() => {
return (
<div className="tldraw__editor">
<Tldraw
persistenceKey="slideshow_example"
shapeUtils={[SlideShapeUtil]}
tools={[SlideShapeTool]}
components={components}
overrides={overrides}
/>
</div>
)
})
export default SlidesExample

Wyświetl plik

@ -0,0 +1,32 @@
import { TldrawUiButton, stopEventPropagation, track, useEditor, useValue } from 'tldraw'
import { moveToSlide, useCurrentSlide, useSlides } from './useSlides'
export const SlidesPanel = track(() => {
const editor = useEditor()
const slides = useSlides()
const currentSlide = useCurrentSlide()
const selectedShapes = useValue('selected shapes', () => editor.getSelectedShapes(), [editor])
if (slides.length === 0) return null
return (
<div className="slides-panel scroll-light" onPointerDown={(e) => stopEventPropagation(e)}>
{slides.map((slide, i) => {
const isSelected = selectedShapes.includes(slide)
return (
<TldrawUiButton
key={'slides-panel-button:' + slide.id}
type="normal"
className="slides-panel-button"
onClick={() => moveToSlide(editor, slide)}
style={{
background: currentSlide?.id === slide.id ? 'var(--color-background)' : 'transparent',
outline: isSelected ? 'var(--color-selection-stroke) solid 1.5px' : 'none',
}}
>
{`Slide ${i + 1}`}
</TldrawUiButton>
)
})}
</div>
)
})

Wyświetl plik

@ -0,0 +1,32 @@
.slides-panel {
display: flex;
flex-direction: column;
gap: 4px;
max-height: calc(100% - 110px);
margin: 50px 0px;
padding: 4px;
background-color: var(--color-low);
pointer-events: all;
border-top-right-radius: var(--radius-4);
border-bottom-right-radius: var(--radius-4);
overflow: auto;
border-right: 2px solid var(--color-background);
border-bottom: 2px solid var(--color-background);
border-top: 2px solid var(--color-background);
}
.slides-panel-button {
border-radius: var(--radius-4);
outline-offset: -1px;
}
.slide-shape-label {
pointer-events: all;
position: absolute;
background: var(--color-low);
padding: calc(12px * var(--tl-scale));
border-bottom-right-radius: calc(var(--radius-4) * var(--tl-scale));
font-size: calc(12px * var(--tl-scale));
color: var(--color-text);
white-space: nowrap;
}

Wyświetl plik

@ -0,0 +1,28 @@
import { EASINGS, Editor, atom, useEditor, useValue } from 'tldraw'
import { SlideShape } from './SlideShapeUtil'
export const $currentSlide = atom<SlideShape | null>('current slide', null)
export function moveToSlide(editor: Editor, slide: SlideShape) {
const bounds = editor.getShapePageBounds(slide.id)
if (!bounds) return
$currentSlide.set(slide)
editor.selectNone()
editor.zoomToBounds(bounds, { duration: 500, easing: EASINGS.easeInOutCubic, inset: 0 })
}
export function useSlides() {
const editor = useEditor()
return useValue<SlideShape[]>('slide shapes', () => getSlides(editor), [editor])
}
export function useCurrentSlide() {
return useValue($currentSlide)
}
export function getSlides(editor: Editor) {
return editor
.getSortedChildIdsForParent(editor.getCurrentPageId())
.map((id) => editor.getShape(id))
.filter((s) => s?.type === 'slide') as SlideShape[]
}

Wyświetl plik

@ -1,6 +1,8 @@
import { Tldraw } from 'tldraw'
import { TLStoreSnapshot, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
import jsonSnapshot from './snapshot.json'
import _jsonSnapshot from './snapshot.json'
const jsonSnapshot = _jsonSnapshot as TLStoreSnapshot
// There's a guide at the bottom of this file!

Wyświetl plik

@ -1,4 +1,3 @@
import { ShapePropsType } from '@tldraw/tlschema/src/shapes/TLBaseShape'
import {
DefaultColorStyle,
DefaultFontStyle,
@ -9,6 +8,7 @@ import {
Geometry2d,
LABEL_FONT_SIZES,
Polygon2d,
ShapePropsType,
ShapeUtil,
T,
TEXT_PROPS,
@ -20,11 +20,11 @@ import {
TextLabel,
Vec,
ZERO_INDEX_KEY,
getDefaultColorTheme,
resizeBox,
structuredClone,
vecModelValidator,
} from 'tldraw'
import { useDefaultColorTheme } from 'tldraw/src/lib/shapes/shared/ShapeFill'
import { getSpeechBubbleVertices, getTailIntersectionPoint } from './helpers'
// Copied from tldraw/tldraw
@ -176,11 +176,11 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
type,
props: { color, font, size, align, text },
} = shape
const theme = getDefaultColorTheme({
isDarkMode: this.editor.user.getIsDarkMode(),
})
const vertices = getSpeechBubbleVertices(shape)
const pathData = 'M' + vertices[0] + 'L' + vertices.slice(1) + 'Z'
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
// eslint-disable-next-line react-hooks/rules-of-hooks
const theme = useDefaultColorTheme()
return (
<>
@ -192,7 +192,6 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
fill={'none'}
/>
</svg>
<TextLabel
id={id}
type={type}
@ -202,7 +201,8 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
align={align}
verticalAlign="start"
text={text}
labelColor={color}
labelColor={theme[color].solid}
isSelected={isSelected}
wrap
/>
</>

Wyświetl plik

@ -0,0 +1,12 @@
---
title: Tool with child states
component: ./ToolWithChildStatesExample.tsx
category: shapes/tools
priority: 2
---
You can implement more complex behaviour in a custom tool by using child states
---
Tools are nodes in tldraw's state machine. They are responsible for handling user input. You can create custom tools by extending the StateNode class and overriding its methods. In this example we expand on the sticker tool from the custom tool example to show how to create a tool that can handle more complex interactions by using child states.

Wyświetl plik

@ -0,0 +1,258 @@
import {
StateNode,
TLEventHandlers,
TLShapePartial,
TLTextShape,
Tldraw,
createShapeId,
} from 'tldraw'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this file!
const OFFSET = -12
// [1]
class StickerTool extends StateNode {
static override id = 'sticker'
static override initial = 'idle'
static override children = () => [Idle, Pointing, Dragging]
}
// [2]
class Idle extends StateNode {
static override id = 'idle'
//[a]
override onEnter = () => {
this.editor.setCursor({ type: 'cross' })
}
//[b]
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
const { editor } = this
switch (info.target) {
case 'canvas': {
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
if (hitShape) {
this.onPointerDown({
...info,
shape: hitShape,
target: 'shape',
})
return
}
this.parent.transition('pointing', { shape: null })
break
}
case 'shape': {
if (editor.inputs.shiftKey) {
editor.updateShape({
id: info.shape.id,
type: 'text',
props: { text: '👻 boo!' },
})
} else {
this.parent.transition('pointing', { shape: info.shape })
}
break
}
}
}
//[c]
override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => {
const { editor } = this
if (info.phase !== 'up') return
switch (info.target) {
case 'canvas': {
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
if (hitShape) {
this.onDoubleClick({
...info,
shape: hitShape,
target: 'shape',
})
return
}
const { currentPagePoint } = editor.inputs
editor.createShape({
type: 'text',
x: currentPagePoint.x + OFFSET,
y: currentPagePoint.y + OFFSET,
props: { text: '❤️' },
})
break
}
case 'shape': {
editor.deleteShapes([info.shape.id])
break
}
}
}
}
// [3]
class Pointing extends StateNode {
static override id = 'pointing'
private shape: TLTextShape | null = null
override onEnter = (info: { shape: TLTextShape | null }) => {
this.shape = info.shape
}
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
this.parent.transition('idle')
}
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
if (this.editor.inputs.isDragging) {
this.parent.transition('dragging', { shape: this.shape })
}
}
}
// [4]
class Dragging extends StateNode {
static override id = 'dragging'
// [a]
private shape: TLShapePartial | null = null
private emojiArray = ['❤️', '🔥', '👍', '👎', '😭', '🤣']
// [b]
override onEnter = (info: { shape: TLShapePartial }) => {
const { currentPagePoint } = this.editor.inputs
const newShape = {
id: createShapeId(),
type: 'text',
x: currentPagePoint.x + OFFSET,
y: currentPagePoint.y + OFFSET,
props: { text: '❤️' },
}
if (info.shape) {
this.shape = info.shape
} else {
this.editor.createShape(newShape)
this.shape = { ...newShape }
}
}
//[c]
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
this.parent.transition('idle')
}
//[d]
override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
const { shape } = this
const { originPagePoint, currentPagePoint } = this.editor.inputs
const distance = originPagePoint.dist(currentPagePoint)
if (shape) {
this.editor.updateShape({
id: shape.id,
type: 'text',
props: {
text: this.emojiArray[Math.floor(distance / 20) % this.emojiArray.length],
},
})
}
}
}
// [5]
const customTools = [StickerTool]
export default function ToolWithChildStatesExample() {
return (
<div className="tldraw__editor">
<Tldraw
// Pass in the array of custom tool classes
tools={customTools}
// Set the initial state to the sticker tool
initialState="sticker"
// hide the ui
hideUi
// Put some helpful text on the canvas
onMount={(editor) => {
editor.createShape({
type: 'text',
x: 50,
y: 50,
props: {
text: '-Double click the canvas to add a sticker\n-Double click a sticker to delete it\n-Click and drag on a sticker to change it\n-Click and drag on the canvas to create a sticker\n-Shift click a sticker for a surprise!',
size: 's',
align: 'start',
},
})
}}
/>
</div>
)
}
/*
Introduction:
Tools are nodes in tldraw's state machine. They are responsible for handling user input.
You can create custom tools by extending the `StateNode` class and overriding its
methods. In this example we expand on the sticker tool from the custom tool example to
show how to create a tool that can handle more complex interactions by using child states.
[1]
This is our custom tool. It has three child states: `Idle`, `Pointing`, and `Dragging`.
We need to define the `id` and `initial` properties, the id is a unique string that
identifies the tool to the editor, and the initial property is the initial state of the
tool. We also need to define a `children` method that returns an array of the tool's
child states.
[2]
This is our Idle state. It is the initial state of the tool. It's job is to figure out
what the user is trying to do and transition to the appropriate state. When transitioning
between states we can use the second argument to pass data to the new state. It has three
methods:
[a] `onEnter`
When entering any state, the `onEnter` method is called. In this case, we set the cursor to
a crosshair.
[b] `onPointerDown`
This method is called when the user presses the mouse button. The target parameter is always
the canvas, so we can use an editor method to check if we're over a shape, and call the
method again with the shape as the target. If we are over a shape, we transition to the
`pointing` state with the shape in the info object. If we're over a shape and holding the
shift key, we update the shape's text. If we're over the canvas, we transition to the
`pointing` state with a null shape in the info object.
[c] `onDoubleClick`
This method is called when the user double clicks the mouse button. We're using some similar
logic here to check if we're over a shape, and if we are, we delete it. If we're over the canvas,
we create a new shape.
[3]
This is our `Pointing` state. It's a transitionary state, we use it to store the shape we're pointing
at, and transition to the dragging state if the user starts dragging. It has three methods:
[a] `onEnter`
When entering this state, we store the shape we're pointing at by getting it from the info object.
[b] `onPointerUp`
This method is called when the user releases the mouse button. We transition to the `idle` state.
[c] `onPointerMove`
This method is called when the user moves the mouse. If the user starts dragging, we transition to
the `dragging` state and pass the shape we're pointing at.
[4]
This is our `Dragging` state. It's responsible for creating and updating the shape that the user is
dragging.
[a] `onEnter`
When entering this state, we create a new shape if we're not dragging an existing one. If we are,
we store the shape we're dragging.
[b] `onPointerUp`
This method is called when the user releases the mouse button. We transition to the `idle` state.
[c] `onPointerMove`
This method is called when the user moves the mouse. We use the distance between the origin and
current mouse position to cycle through an array of emojis and update the shape's text.
[5]
We pass our custom tool to the `Tldraw` component as an array. We also set the initial state to our
custom tool. For the purposes of this demo, we're also hiding the UI and adding some helpful text to
the canvas.
*/

Wyświetl plik

@ -21,7 +21,7 @@ setDefaultEditorAssetUrls(assetUrls)
setDefaultUiAssetUrls(assetUrls)
const gettingStartedExamples = examples.find((e) => e.id === 'Getting started')
if (!gettingStartedExamples) throw new Error('Could not find getting started exmaples')
const basicExample = gettingStartedExamples.value.find((e) => e.title === 'Persistence key')
const basicExample = gettingStartedExamples.value.find((e) => e.title === 'Tldraw component')
if (!basicExample) throw new Error('Could not find initial example')
const router = createBrowserRouter([

Wyświetl plik

@ -1,3 +1,21 @@
## 2.0.30
- Fixes a bug that prevented opening some files.
## 2.0.29
- Improved note shapes.
- Color improvements for both light and dark mode.
- Bug fixes and performance improvements.
## 2.0.28
- Fix an issue with panning the canvas.
## 2.0.27
- Bug fixes and performance improvements.
## 2.0.26
- Bug fixes and performance improvements.

Wyświetl plik

@ -1,7 +1,7 @@
{
"name": "tldraw-vscode",
"description": "The tldraw extension for VS Code.",
"version": "2.0.26",
"version": "2.0.30",
"private": true,
"author": {
"name": "tldraw Inc.",

Wyświetl plik

@ -1,3 +1,4 @@
<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="M23.8539 0.315423C24.2744 -0.105141 24.9563 -0.105141 25.3769 0.315423L29.6846 4.62312C30.1051 5.04368 30.1051 5.72555 29.6846 6.14612L21.1928 14.6379C21.0291 14.8016 20.84 14.9379 20.633 15.0414L12.1739 19.2709C11.7593 19.4782 11.2585 19.397 10.9308 19.0692C10.603 18.7414 10.5217 18.2407 10.729 17.8261L14.9586 9.367C15.0621 9.15995 15.1984 8.97093 15.362 8.80723L23.8539 0.315423ZM24.6154 2.59992L16.8851 10.3302L14.6488 14.8027L15.1973 15.3511L19.6697 13.1149L27.4001 5.38462L24.6154 2.59992ZM19.2308 2.15384L17.0769 4.30769H8.24617C7.32369 4.30769 6.69661 4.30853 6.2119 4.34813C5.73976 4.38671 5.4983 4.45663 5.32987 4.54245C4.9246 4.74894 4.5951 5.07844 4.38861 5.48371C4.30279 5.65214 4.23287 5.89359 4.19429 6.36573C4.15469 6.85044 4.15385 7.47753 4.15385 8.4V21.7538C4.15385 22.6763 4.15469 23.3034 4.19429 23.7881C4.23287 24.2603 4.30279 24.5017 4.38861 24.6701C4.5951 25.0754 4.9246 25.4049 5.32987 25.6114C5.4983 25.6972 5.73976 25.7671 6.2119 25.8057C6.69661 25.8453 7.32369 25.8462 8.24617 25.8462H21.6C22.5225 25.8462 23.1496 25.8453 23.6343 25.8057C24.1064 25.7671 24.3479 25.6972 24.5163 25.6114C24.9216 25.4049 25.2511 25.0754 25.4576 24.6701C25.5434 24.5017 25.6133 24.2603 25.6519 23.7881C25.6915 23.3034 25.6923 22.6763 25.6923 21.7538V12.923L27.8462 10.7692V21.7538V21.7983C27.8462 22.6652 27.8462 23.3807 27.7986 23.9635C27.7491 24.5688 27.643 25.1253 27.3767 25.648C26.9637 26.4585 26.3047 27.1175 25.4941 27.5305C24.9715 27.7968 24.415 27.9029 23.8097 27.9524C23.2269 28 22.5114 28 21.6445 28H21.6H8.24617H8.20166C7.33478 28 6.61932 28 6.0365 27.9524C5.43117 27.9029 4.87472 27.7968 4.35205 27.5305C3.5415 27.1175 2.88251 26.4585 2.46951 25.648C2.2032 25.1253 2.09705 24.5688 2.0476 23.9635C1.99998 23.3807 1.99999 22.6652 2 21.7984V21.7983V21.7538V8.4V8.35552V8.35546V8.35545C1.99999 7.48859 1.99998 6.77315 2.0476 6.19034C2.09705 5.585 2.2032 5.02855 2.46951 4.50589C2.88251 3.69534 3.5415 3.03635 4.35205 2.62336C4.87472 2.35704 5.43117 2.2509 6.0365 2.20144C6.61932 2.15382 7.33476 2.15383 8.20163 2.15384H8.20169H8.24617H19.2308Z" fill="black"/>
<path d="M17 27V19C17 17.8954 17.8954 17 19 17H27" stroke="black" stroke-width="2"/>
<path d="M17.5789 26.45L26.3775 18.0914C26.775 17.7138 27 17.1896 27 16.6414V5C27 3.89543 26.1046 3 25 3H5C3.89543 3 3 3.89543 3 5V25C3 26.1046 3.89543 27 5 27H16.2014C16.7141 27 17.2072 26.8031 17.5789 26.45Z" stroke="black" stroke-width="2"/>
</svg>

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.2 KiB

Po

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

Wyświetl plik

@ -111,6 +111,7 @@
"action.zoom-to-selection": "Zoom to selection",
"assets.files.upload-failed": "Upload failed",
"assets.url.failed": "Couldn't load URL preview",
"color-style.white": "White",
"color-style.black": "Black",
"color-style.blue": "Blue",
"color-style.green": "Green",

Wyświetl plik

@ -21,7 +21,7 @@ module.exports = {
},
],
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
testRegex: '.+\\.(test|spec)\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
modulePathIgnorePatterns: [
'<rootDir>/test/__fixtures__',

Wyświetl plik

@ -11,6 +11,10 @@ import { TextDecoder, TextEncoder } from 'util'
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
Image.prototype.decode = async function () {
return true
}
function convertNumbersInObject(obj: any, roundToNearest: number) {
if (!obj) return obj
if (Array.isArray(obj)) {

Wyświetl plik

@ -71,7 +71,7 @@
]
},
"devDependencies": {
"@microsoft/api-extractor": "^7.41.0",
"@microsoft/api-extractor": "^7.43.1",
"@next/eslint-plugin-next": "^13.3.0",
"@swc/core": "^1.3.55",
"@swc/jest": "^0.2.34",
@ -96,6 +96,7 @@
"jest": "30.0.0-alpha.2",
"json5": "^2.2.3",
"lazyrepo": "0.0.0-alpha.27",
"license-report": "^6.5.0",
"lint-staged": ">=10",
"prettier": "^3.0.3",
"prettier-plugin-organize-imports": "^3.2.3",

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