kopia lustrzana https://github.com/Tldraw/Tldraw
Sync docs (#4164)
An initial pass at sync docs ### Change type - [x] `other` --------- Co-authored-by: Adam Wiggins <a@adamwiggins.com> Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>pull/4259/head
rodzic
9740f865a5
commit
ff8657fd0e
|
@ -4,7 +4,6 @@ import { SandpackCodeViewer, SandpackFiles, SandpackProvider } from '@codesandbo
|
|||
import { getOwnProperty } from '@tldraw/utils'
|
||||
import { useTheme } from 'next-themes'
|
||||
import React, {
|
||||
Fragment,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
cloneElement,
|
||||
|
@ -19,7 +18,7 @@ import { A } from './generic'
|
|||
|
||||
const CodeLinksContext = createContext<Record<string, string>>({})
|
||||
|
||||
export function CodeLinkProvider({
|
||||
export function CodeLinks({
|
||||
children,
|
||||
links,
|
||||
}: {
|
||||
|
@ -29,8 +28,14 @@ export function CodeLinkProvider({
|
|||
return <CodeLinksContext.Provider value={links}>{children}</CodeLinksContext.Provider>
|
||||
}
|
||||
|
||||
const FocusLinesContext = createContext<null | number[]>(null)
|
||||
export function FocusLines({ children, lines }: { children: ReactNode; lines: number[] }) {
|
||||
return <FocusLinesContext.Provider value={lines}>{children}</FocusLinesContext.Provider>
|
||||
}
|
||||
|
||||
export function Code({ children, ...props }: React.ComponentProps<'code'>) {
|
||||
const codeLinks = useContext(CodeLinksContext)
|
||||
const focusLines = useContext(FocusLinesContext)
|
||||
|
||||
const newChildren = useMemo(() => {
|
||||
// to linkify code, we have to do quite a lot of work. we need to take the output of
|
||||
|
@ -88,6 +93,9 @@ export function Code({ children, ...props }: React.ComponentProps<'code'>) {
|
|||
return tokens
|
||||
}
|
||||
|
||||
// what line are we on?
|
||||
let lineNumber = 1
|
||||
|
||||
// recursively process the children array
|
||||
function processChildrenArray(children: ReactNode): ReactNode {
|
||||
if (!Array.isArray(children)) {
|
||||
|
@ -121,15 +129,29 @@ export function Code({ children, ...props }: React.ComponentProps<'code'>) {
|
|||
}
|
||||
|
||||
function pushInCurrentSpan(content: ReactNode) {
|
||||
const lineProps = {
|
||||
'data-line': lineNumber,
|
||||
'data-line-state': focusLines
|
||||
? focusLines.includes(lineNumber)
|
||||
? 'focus'
|
||||
: 'blur'
|
||||
: undefined,
|
||||
}
|
||||
|
||||
if (lastSeenHighlightSpan) {
|
||||
newChildren.push(
|
||||
cloneElement(lastSeenHighlightSpan, {
|
||||
key: newChildren.length,
|
||||
children: content,
|
||||
...lineProps,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
newChildren.push(<Fragment key={newChildren.length}>{content}</Fragment>)
|
||||
newChildren.push(
|
||||
<span key={newChildren.length} {...lineProps}>
|
||||
{content}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,7 +183,16 @@ export function Code({ children, ...props }: React.ComponentProps<'code'>) {
|
|||
}
|
||||
} else {
|
||||
finishCurrentIdentifier()
|
||||
pushInCurrentSpan(token)
|
||||
|
||||
const lineParts = token.split('\n')
|
||||
for (let i = 0; i < lineParts.length; i++) {
|
||||
let part = lineParts[i]
|
||||
if (i > 0) {
|
||||
lineNumber += 1
|
||||
part = '\n' + part
|
||||
}
|
||||
pushInCurrentSpan(part)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,7 +228,7 @@ export function Code({ children, ...props }: React.ComponentProps<'code'>) {
|
|||
}
|
||||
|
||||
return processChildrenArray(children)
|
||||
}, [children, codeLinks])
|
||||
}, [children, codeLinks, focusLines])
|
||||
|
||||
return <code {...props}>{newChildren}</code>
|
||||
}
|
||||
|
|
|
@ -142,7 +142,7 @@ export const Video = (props: any) => {
|
|||
/* ------------------- Code Blocks ------------------ */
|
||||
|
||||
export const Pre = (props: any) => {
|
||||
if (props.children?.props?.className.startsWith('language-')) {
|
||||
if (props.children?.props?.className?.startsWith('language-')) {
|
||||
return props.children
|
||||
}
|
||||
|
||||
|
@ -173,8 +173,8 @@ export const Embed = (props: any) => {
|
|||
export const Callout = ({ icon, children }: any) => {
|
||||
return (
|
||||
<div className="article__callout">
|
||||
<span>{icon}</span>
|
||||
<p>{children}</p>
|
||||
<div className="article__callout__icon">{icon}</div>
|
||||
<div className="article__callout__message">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as customComponents from '../article-components'
|
||||
import * as apiComponents from './api-docs'
|
||||
import { Code, CodeBlock, CodeLinkProvider } from './code'
|
||||
import { Code, CodeBlock, CodeLinks, FocusLines } from './code'
|
||||
import {
|
||||
A,
|
||||
ApiHeading,
|
||||
|
@ -32,7 +32,8 @@ export const components = {
|
|||
a: A,
|
||||
blockquote: Blockquote,
|
||||
code: Code,
|
||||
CodeLinkProvider,
|
||||
CodeLinks: CodeLinks,
|
||||
FocusLines: FocusLines,
|
||||
h1: Heading1,
|
||||
h2: Heading2,
|
||||
h3: Heading3,
|
||||
|
|
|
@ -3,7 +3,7 @@ title: Assets
|
|||
status: published
|
||||
author: steveruizok
|
||||
date: 6/9/2023
|
||||
order: 3
|
||||
order: 5
|
||||
keywords:
|
||||
- image
|
||||
- video
|
||||
|
@ -13,14 +13,30 @@ keywords:
|
|||
- files
|
||||
---
|
||||
|
||||
Assets are dynamic records that store data about a shared asset. For example, our image and video shapes refer to assets rather than embedding their source files directly. This is essential because, by default, these asset sources are stored in base64 data.
|
||||
Assets are dynamic records that store data about a shared asset. For example, our image and video
|
||||
shapes refer to assets rather than embedding their source files directly. Asset storage and
|
||||
retrieval is controlled by [`TLAssetStore`](?). Different [`TLStore`](?) setups require different asset setups:
|
||||
|
||||
You can use assets for any shared piece of information, however they're best used for images, videos, and files.
|
||||
- By default, the store is in-memory only, so [`inlineBase64AssetStore`](?) converts images to data
|
||||
URLs.
|
||||
- When using the [`persistenceKey` prop](/docs/persistence#The-persistenceKey-prop), the store is
|
||||
synced to the browser's local
|
||||
[IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), so we store images
|
||||
there too.
|
||||
- When using a [multiplayer sync server](/docs/sync), you would implement TLAssetStore to upload
|
||||
images to e.g. an S3 bucket.
|
||||
|
||||
## Examples
|
||||
|
||||
While we're working on docs for this part of the project, please refer to the examples below:
|
||||
|
||||
- [Using images hosted](https://github.com/tldraw/tldraw/blob/main/apps/examples/src/examples/hosted-images/HostedImagesExample.tsx)
|
||||
- [Customizing the default asset options](https://github.com/tldraw/tldraw/blob/main/apps/examples/src/examples/asset-props/AssetPropsExample.tsx)
|
||||
- [Handling pasted / dropped external content](https://github.com/tldraw/tldraw/blob/main/apps/examples/src/examples/external-content-sources/ExternalContentSourcesExample.tsx)
|
||||
- [Using images
|
||||
hosted](/examples/data/assets/hosted-images)
|
||||
- [Customizing the default asset
|
||||
options](/examples/data/assets/asset-props)
|
||||
- [Handling pasted / dropped external
|
||||
content](/examples/data/assets/external-content-sources)
|
||||
- [A simple asset store that uploads content to a remote
|
||||
server](https://github.com/tldraw/tldraw/blob/main/apps/simple-server-example/src/client/App.tsx)
|
||||
- [A more complex asset store that optimizes images when retrieving
|
||||
them](https://github.com/tldraw/tldraw/blob/main/packages/sync/src/useMutliplayerDemo.ts#L87)
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
---
|
||||
title: Collaboration
|
||||
status: published
|
||||
author: ds300
|
||||
date: 3/22/2023
|
||||
order: 8
|
||||
---
|
||||
|
||||
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/sync-core). 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.
|
|
@ -3,7 +3,7 @@ title: Persistence
|
|||
status: published
|
||||
author: steveruizok
|
||||
date: 3/22/2023
|
||||
order: 6
|
||||
order: 4
|
||||
keywords:
|
||||
- data
|
||||
- sync
|
||||
|
@ -13,7 +13,9 @@ keywords:
|
|||
- localstorage
|
||||
---
|
||||
|
||||
Persistence in tldraw means storing information about the editor's state to a database and then restoring it later. There are a few options that developers have for getting data into tldraw and out again.
|
||||
In tldraw, persitence means storing information about the editor's state to a database and then restoring it later.
|
||||
|
||||
The simplest implementation is the browser's local storage. But this also provides the hooks for a sync engine, which can send realtime incremenal updates the canvas to your backend server, allowing multiple people to collaborate on the canvas.
|
||||
|
||||
## The `"persistenceKey"` prop
|
||||
|
||||
|
@ -406,18 +408,6 @@ const migrations = createShapePropsMigrationSequence({
|
|||
|
||||
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/sync-core).
|
||||
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.
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
title: Sync
|
||||
status: published
|
||||
author: alex
|
||||
date: 7/11/2024
|
||||
order: 6
|
||||
keywords:
|
||||
- data
|
||||
- sync
|
||||
- persistence
|
||||
- database
|
||||
- multiplayer
|
||||
- collaboration
|
||||
- server
|
||||
- websockets
|
||||
---
|
||||
|
||||
Allow multiple users to collaborate on the canvas with **tldraw sync**, an optional module included with tldraw.
|
||||
|
||||
<div className="article__image" style={{ border: 'none' }}>
|
||||
<img
|
||||
alt="Multiplayer example using tldraw sync"
|
||||
src="/images/multiplayer.gif"
|
||||
style={{
|
||||
// learn this one weird trick for transparent animated gifs. doctors hate her!!!
|
||||
mask: 'url(/images/multiplayer_mask.png) luminance center 100% / 100% no-repeat',
|
||||
WebkitMask: 'url(/images/multiplayer_mask.png) luminance center 100% / 100% no-repeat',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Changes to a document are synchronized in realtime to other users, including presence information like live cursors. This requires you to run a backend server, although we provide a demo server you can use for testing.
|
||||
|
||||
## Try the demo server
|
||||
|
||||
The easiest way to start experimenting with multiplayer is with our demo server. Start by installing
|
||||
`@tldraw/sync`:
|
||||
|
||||
```bash
|
||||
npm install @tldraw/sync
|
||||
```
|
||||
|
||||
Then, in your app, call the `useMutiplayerDemo` hook with a room ID. It'll return a
|
||||
[`store`](/docs/persistence#The-store-prop) that you can pass into the tldraw component:
|
||||
|
||||
```tsx
|
||||
function MyApp() {
|
||||
const store = useSyncDemo({ roomId: 'myApp-abc123' })
|
||||
return <Tldraw store={store} />
|
||||
}
|
||||
```
|
||||
|
||||
Make sure the room ID is unique. Anyone who uses the same ID can access your room. Our sync demo
|
||||
server is a great way to prototype multiplayer in tldraw, but it shouldn't be used in production.
|
||||
Rooms will be deleted after 24 hours.
|
||||
|
||||
## Self-host the backend
|
||||
|
||||
To use tldraw sync in production, you need to host the backend on your own server. There are three parts to sync in
|
||||
tldraw:
|
||||
|
||||
1. The sync server, which serves the room over [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API).
|
||||
2. Somewhere to large assets that are too big to send over websockets, like images and videos.
|
||||
3. The sync client, which connects to the server with the [`useSync`](?) hook.
|
||||
|
||||
We recommend starting from our [Cloudflare Workers
|
||||
template](https://github.com/tldraw/cloudflare-workers-template). This is a minimal setup of the
|
||||
system that powers multiplayer collaboration for hundreds of thousands of rooms & users on
|
||||
tldraw.com. It includes everything you need to get up and running with multiplayer tldraw quickly:
|
||||
the sync server, asset hosting, and URL unfurling.
|
||||
|
||||
### TLSocketRoom
|
||||
|
||||
The core part of our sync server is [`TLSocketRoom`](?). It works with any JavaScript-based web
|
||||
framework with support for WebSockets, and any storage backend: it's up to you to wire in those
|
||||
parts yourself.
|
||||
|
||||
Make sure there's only ever one `TLSocketRoom` for each room in your app. If there's
|
||||
more than one, users won't see each other and will overwrite others' changes. We use [Durable
|
||||
Objects](https://developers.cloudflare.com/durable-objects/) to achieve this on tldraw.com.
|
||||
|
||||
If you don't want to our Cloudflare template, we also provide a [simple Node/Bun server
|
||||
example](https://github.com/tldraw/tldraw/tree/main/apps/simple-server-example). This is not
|
||||
production ready, but you can use it as a reference implementation to build it into your own
|
||||
existing backend.
|
||||
|
||||
Read the reference docs for [`TLSocketRoom`](?) for more.
|
||||
|
||||
### Asset storage
|
||||
|
||||
As well as synchronizing the rapidly-changing document data, tldraw also needs a way to store and
|
||||
retrieve large binary assets like images or videos.
|
||||
|
||||
You'll need to make sure your backend can handle asset uploads & downloads, then implement
|
||||
[`TLAssetStore`](?) to connect it to tldraw.
|
||||
|
||||
- Read about [how assets work in tldraw](/docs/assets).
|
||||
- Read the [`TLAssetStore`](?) reference docs.
|
||||
|
||||
### The sync client
|
||||
|
||||
The [`useSync`](?) hook connects your backend to the tldraw editor. Call the hook to create a store,
|
||||
then pass it in to the `<Tldraw />` component:
|
||||
|
||||
```tsx
|
||||
function MyApp() {
|
||||
const store = useSync({
|
||||
uri: 'https://myapp.com/tlsocketroom/my-room-id',
|
||||
assets: myAssetStore,
|
||||
})
|
||||
|
||||
return <Tldraw store={store} />
|
||||
}
|
||||
|
||||
const myAssetStore: TLAssetStore {
|
||||
upload(file, asset) {
|
||||
return uploadFileAndReturnUrl(file)
|
||||
},
|
||||
resolve(asset) {
|
||||
return asset.props.src
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The sync client automatically enables some default collaborative UX which you can customize using
|
||||
editor [component overrides](/reference/tldraw/TLComponents):
|
||||
|
||||
- Collaborator cursors: [`TLComponents#CollaboratorCursor`](?)
|
||||
- Cursor chat: [`TLComponents#CursorChatBubble`](?)
|
||||
- Offline indicator: [`TLComponents#TopPanel`](?)
|
||||
- Collaborator list: [`TLComponents#SharePanel`](?)
|
||||
|
||||
## Integrating other sync systems
|
||||
|
||||
While tldraw multiplayer is the easiest way to add sync capabilities to your tldraw canvas, you can
|
||||
implement your own or use another off-the-shelf system.
|
||||
|
||||
One example is this [Yjs integration](https://github.com/tldraw/tldraw-yjs-example). Refer to the
|
||||
[Persistence docs](/docs/persistence) for how to hook it together with your sync layer of choice.
|
|
@ -3,7 +3,7 @@ title: Tools
|
|||
status: published
|
||||
author: steveruizok
|
||||
date: 3/22/2023
|
||||
order: 5
|
||||
order: 3
|
||||
keywords:
|
||||
- custom
|
||||
- tools
|
||||
|
|
|
@ -3,7 +3,7 @@ title: User interface
|
|||
status: published
|
||||
author: steveruizok
|
||||
date: 3/22/2023
|
||||
order: 7
|
||||
order: 3
|
||||
keywords:
|
||||
- ui
|
||||
- interface
|
||||
|
|
|
@ -71,26 +71,6 @@ If you're using the [Tldraw](?) component in a full-screen app, you probably als
|
|||
|
||||
This may not be critical to [Tldraw](?) performing correctly, however some features (such as safe area positioning) will only work correctly if these viewport options are set.
|
||||
|
||||
## Server Rendering
|
||||
|
||||
The [Tldraw](?) component can't be server-rendered. If you're using the component in a server-rendered framework (such as Next.js) then you need to import it dynamically.
|
||||
|
||||
```tsx
|
||||
const Tldraw = dynamic(async () => (await import('tldraw')).Tldraw, { ssr: false })
|
||||
```
|
||||
|
||||
### Using a bundler
|
||||
|
||||
If you're using a bundler like webpack or rollup, you can import the assets directly from the `assets` package. Here you can use `getAssetUrlsByMetaUrl` helper function:
|
||||
|
||||
```tsx
|
||||
import { getAssetUrlsByMetaUrl } from 'assets/urls'
|
||||
|
||||
const assetUrls = getAssetUrlsByMetaUrl()
|
||||
|
||||
<Tldraw assetUrls={assetUrls} />
|
||||
```
|
||||
|
||||
## Usage in Frameworks
|
||||
|
||||
Visit our [framework examples repository](https://github.com/tldraw/examples) to see examples of tldraw being used in various frameworks.
|
||||
|
@ -99,6 +79,26 @@ Visit our [framework examples repository](https://github.com/tldraw/examples) to
|
|||
|
||||
In order to use the [Tldraw](?) component, the app must be able to find certain assets. These are contained in the `embed-icons`, `fonts`, `icons`, and `translations` folders. We offer a few different ways of making these assets available to your app.
|
||||
|
||||
### Using a bundler
|
||||
|
||||
If you're using a bundler like webpack or rollup, you can import the assets directly from the `@tldraw/assets` package. Your bundler should be able to copy across the assets with your bundle, and insert the correct URLs in their place.
|
||||
|
||||
There are three options:
|
||||
|
||||
- `import {getAssetUrlsByImport} from '@tldraw/assets/imports`: import asset files using import statements. You'll need to configure your bundler to treat imports of .svg, .png, .json, and .woff2 files as external assets.
|
||||
- `import {getAssetUrlsByImport} from '@tldraw/assets/imports.vite`: import asset files, appending `?url` to the asset path. This works correctly with [vite](https://vitejs.dev/guide/assets#explicit-url-imports) without any extra configuration needed.
|
||||
- `import {getAssetUrlsByMeta} from '@tldraw/assets/urls`: get asset urls using `new URL(path, import.meta.url)`. This is a standards-based approach that works natively in most modern browsers & bundlers.
|
||||
|
||||
Call the function, and pass the resulting `assetUrls` into the `Tldraw` component:
|
||||
|
||||
```tsx
|
||||
import { getAssetUrlsByMetaUrl } from '@tldraw/assets/urls'
|
||||
|
||||
const assetUrls = getAssetUrlsByMetaUrl()
|
||||
|
||||
<Tldraw assetUrls={assetUrls} />
|
||||
```
|
||||
|
||||
### Using a public CDN
|
||||
|
||||
By default we serve these assets from a public CDN. Everything should work out of the box and is a good way to get started.
|
||||
|
|
|
@ -12,7 +12,7 @@ You can use the tldraw SDK to craft infinite canvas experiences for the web. It'
|
|||
|
||||
By the end of this guide you will have made something that looks like this:
|
||||
|
||||
<Embed className="article__embed--quickstart" src="https://examples.tldraw.com/develop" />
|
||||
<Embed className="article__embed--quickstart" src="https://examples.tldraw.com/sync-demo" />
|
||||
|
||||
### 1. Installation
|
||||
|
||||
|
@ -23,7 +23,7 @@ By the end of this guide you will have made something that looks like this:
|
|||
npm install tldraw
|
||||
```
|
||||
|
||||
### 2. Import Styles
|
||||
### 2. Import styles
|
||||
|
||||
To import fonts and CSS for tldraw:
|
||||
|
||||
|
@ -39,7 +39,7 @@ body {
|
|||
}
|
||||
```
|
||||
|
||||
### 3. Render Tldraw Component
|
||||
### 3. Render tldraw component
|
||||
|
||||
To render the Tldraw component
|
||||
|
||||
|
@ -62,14 +62,47 @@ export default function App() {
|
|||
}
|
||||
```
|
||||
|
||||
### 4. Sync betweens users
|
||||
|
||||
At this point, you have a complete working single-user whiteboard. To add support for multiple users
|
||||
collaborating in realtime, you can use the **tldraw sync** extension library.
|
||||
|
||||
```bash
|
||||
npm install @tldraw/sync
|
||||
```
|
||||
|
||||
Set up your component with our demo server:
|
||||
|
||||
- Import the `useSyncDemo` hook from the `@tldraw/sync` package
|
||||
- Add the hook to your component with a unique `roomId`
|
||||
- Pass the `store` returned from the hook into your `<Tldraw />` component
|
||||
|
||||
<FocusLines lines={[2, 6, 10]}>
|
||||
|
||||
```javascript
|
||||
import { Tldraw } from 'tldraw'
|
||||
import { useSyncDemo } from '@tldraw/sync'
|
||||
import './index.css'
|
||||
|
||||
export default function App() {
|
||||
const store = useSyncDemo({ roomId: 'myapp-abc123' })
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0 }}>
|
||||
<Tldraw store={store} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</FocusLines>
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you have your canvas working, you may be wondering: what next?
|
||||
Now that you have your canvas working, you can:
|
||||
|
||||
You can try:
|
||||
|
||||
- Giving the editor a makeover by [customizing the UI](/docs/user-interface)
|
||||
- Adding your own [shapes](/docs/shapes) and [tools](/docs/tools)
|
||||
- Providing collaboration using [multiplayer](https://github.com/tldraw/tldraw-yjs-example)
|
||||
- [Customize the UI](/docs/user-interface) of the editor
|
||||
- Add your own [shapes](/docs/shapes) and [tools](/docs/tools)
|
||||
- Replace our sync demo server with a [more robust sync backend](/docs/sync)
|
||||
|
||||
We provide the above examples and more in our [examples section](/examples). Go build something creative and please do share it with us in our [#show-and-tell](https://discord.com/invite/SBBEVCA4PG) channel on Discord!
|
||||
|
|
|
@ -112,6 +112,86 @@
|
|||
],
|
||||
"hero": null
|
||||
},
|
||||
{
|
||||
"id": "sync-core",
|
||||
"title": "@tldraw/sync-core",
|
||||
"description": "",
|
||||
"groups": [
|
||||
{
|
||||
"id": "Class",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Function",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Variable",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Enum",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Interface",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "TypeAlias",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Namespace",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Component",
|
||||
"path": null
|
||||
}
|
||||
],
|
||||
"hero": null
|
||||
},
|
||||
{
|
||||
"id": "sync",
|
||||
"title": "@tldraw/sync",
|
||||
"description": "",
|
||||
"groups": [
|
||||
{
|
||||
"id": "Class",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Function",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Variable",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Enum",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Interface",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "TypeAlias",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Namespace",
|
||||
"path": null
|
||||
},
|
||||
{
|
||||
"id": "Component",
|
||||
"path": null
|
||||
}
|
||||
],
|
||||
"hero": null
|
||||
},
|
||||
{
|
||||
"id": "tldraw",
|
||||
"title": "tldraw",
|
||||
|
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 603 KiB |
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 9.4 KiB |
|
@ -545,13 +545,13 @@ async function excerptToMarkdown(
|
|||
}
|
||||
|
||||
return [
|
||||
`<CodeLinkProvider links={${JSON.stringify(links)}}>`,
|
||||
`<CodeLinks links={${JSON.stringify(links)}}>`,
|
||||
'',
|
||||
'```ts',
|
||||
code,
|
||||
'```',
|
||||
'',
|
||||
'</CodeLinkProvider>',
|
||||
'</CodeLinks>',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
|
@ -608,11 +608,11 @@ function addExtends(result: Result, item: ApiItem, heritage: HeritageType[]) {
|
|||
}
|
||||
|
||||
result.markdown += [
|
||||
`<CodeLinkProvider links={${JSON.stringify(links)}}>`,
|
||||
`<CodeLinks links={${JSON.stringify(links)}}>`,
|
||||
'',
|
||||
`Extends \`${heritage.map((type) => type.excerpt.text).join(', ')}\`.`,
|
||||
'',
|
||||
'</CodeLinkProvider>',
|
||||
'</CodeLinks>',
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
|
|
@ -4,4 +4,6 @@ export const TLDRAW_PACKAGES_TO_INCLUDE_IN_DOCS = [
|
|||
'tldraw',
|
||||
'tlschema',
|
||||
'validate',
|
||||
'sync',
|
||||
'sync-core',
|
||||
]
|
||||
|
|
|
@ -548,7 +548,8 @@ body {
|
|||
margin: 20px 0px;
|
||||
}
|
||||
|
||||
.article > blockquote {
|
||||
.article > blockquote,
|
||||
.article__callout {
|
||||
max-width: 100%;
|
||||
margin: 32px 0px;
|
||||
padding: 16px;
|
||||
|
@ -556,6 +557,21 @@ body {
|
|||
background-color: var(--color-blockquote);
|
||||
}
|
||||
|
||||
.article__callout {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 16px;
|
||||
}
|
||||
.article__callout__icon {
|
||||
/* use the system font so we get proper emoji: */
|
||||
font-family: sans-serif;
|
||||
font-size: 20px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.article__callout__message {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.article pre {
|
||||
max-width: 100%;
|
||||
margin: 32px 0px;
|
||||
|
@ -710,6 +726,7 @@ body {
|
|||
.article__embed--quickstart {
|
||||
aspect-ratio: 16 / 9;
|
||||
min-height: 405px;
|
||||
max-width: 100%;
|
||||
margin: 32px 0px;
|
||||
}
|
||||
|
||||
|
|
|
@ -96,3 +96,8 @@ a.code-link::before {
|
|||
vertical-align: -0.142em; /* 2px when font-size is 14px */
|
||||
margin-right: 0.142em;
|
||||
}
|
||||
|
||||
.hljs [data-line-state='blur'] {
|
||||
color: var(--hl);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export async function autoLinkDocs(db: Database<sqlite3.Database, sqlite3.Statem
|
|||
await Promise.all(articles.map((a) => autoLinkDocsForArticle(db, a)))
|
||||
}
|
||||
|
||||
const regex = /\[([^\[\]]*?)\]\(\?\)/g
|
||||
const regex = /\[`?([^\[\]]*?)`?\]\(\?\)/g
|
||||
|
||||
export async function autoLinkDocsForArticle(
|
||||
db: Database<sqlite3.Database, sqlite3.Statement>,
|
||||
|
@ -42,7 +42,7 @@ export async function autoLinkDocsForArticle(
|
|||
|
||||
if (heading) {
|
||||
const headingRow = await db.get('SELECT slug FROM headings WHERE slug = ?', heading)
|
||||
if (!headingRow) throw Error(`Could not find heading for ${_title} (${heading})`)
|
||||
if (!headingRow) throw Error(`Could not find heading for ${_title} (${heading}) in ${id}`)
|
||||
str = `[\`${title}.${heading}\`](/${article.sectionId}/${article.categoryId}/${article.id}#${headingRow.slug})`
|
||||
} else {
|
||||
str = `[\`${title}\`](/${article.sectionId}/${article.categoryId}/${article.id})`
|
||||
|
|
|
@ -34,7 +34,7 @@ const getExamplesForCategory = (category: Category) =>
|
|||
|
||||
const categories: Record<Category, string> = {
|
||||
basic: 'Getting started',
|
||||
collaboration: 'Collaboration',
|
||||
collaboration: 'Sync',
|
||||
ui: 'UI & theming',
|
||||
'shapes/tools': 'Shapes & tools',
|
||||
'data/assets': 'Data & assets',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: Sync with custom shape
|
||||
title: …with a custom shape
|
||||
component: ./SyncDemoShapeExample.tsx
|
||||
category: collaboration
|
||||
priority: 3
|
||||
|
@ -9,4 +9,4 @@ multiplayer: true
|
|||
|
||||
---
|
||||
|
||||
The `useSyncDemo` hook can be used to quickly prototype multiplayer experiences in tldraw using a demo backend that we host. Data is wiped after one day.
|
||||
This example shows a custom shape in combination with tldraw sync.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: Custom user data
|
||||
title: …with custom user data
|
||||
component: ./SyncCustomUser.tsx
|
||||
category: collaboration
|
||||
priority: 3
|
||||
|
@ -9,4 +9,4 @@ multiplayer: true
|
|||
|
||||
---
|
||||
|
||||
This example shows how to integrate your own user data into tldraw and tldraw's multiplayer system.
|
||||
This example shows how to integrate your own user data into tldraw sync.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: Sync
|
||||
title: Multiplayer sync
|
||||
component: ./SyncDemoExample.tsx
|
||||
category: collaboration
|
||||
priority: 1
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
title: Collaboration with YJS
|
||||
component: ./YjsExample.tsx
|
||||
category: collaboration
|
||||
priority: 2
|
||||
keywords: [multi, player]
|
||||
---
|
|
@ -1,11 +0,0 @@
|
|||
export default function YjsExample() {
|
||||
return (
|
||||
<div className="tldraw__editor" style={{ padding: 32 }}>
|
||||
We’ve moved! See{' '}
|
||||
<a href="https://github.com/tldraw/tldraw-yjs-example">
|
||||
https://github.com/tldraw/tldraw-yjs-example
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
# Node/Bun server example
|
||||
# tldraw sync, simple Node/Bun server example
|
||||
|
||||
This is a simple example of how to integrate tldraw's sync engine with a Node server or a Bun server.
|
||||
This is a simple example of a backend for [tldraw sync](https://tldraw.dev/docs/sync) with a Node or Bun server.
|
||||
|
||||
Run `yarn dev-node` or `yarn dev-bun` in this folder to start the server + client.
|
||||
|
||||
For a production-ready example specific to Cloudflare, see /templates/cloudflare-workers.
|
||||
|
|
|
@ -435,7 +435,6 @@ export const DefaultQuickActions: NamedExoticComponent<TLUiQuickActionsProps>;
|
|||
export function DefaultQuickActionsContent(): JSX_2.Element | undefined;
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
export const defaultShapeTools: readonly [typeof TextShapeTool, typeof DrawShapeTool, typeof GeoShapeTool, typeof NoteShapeTool, typeof LineShapeTool, typeof FrameShapeTool, typeof ArrowShapeTool, typeof HighlightShapeTool];
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -1547,7 +1546,7 @@ export interface TLArrowPoint {
|
|||
point: VecLike;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export interface TLComponents extends TLEditorComponents, TLUiComponents {
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,26 @@ import { usePreloadAssets } from './ui/hooks/usePreloadAssets'
|
|||
import { useTranslation } from './ui/hooks/useTranslation/useTranslation'
|
||||
import { useDefaultEditorAssetsWithOverrides } from './utils/static-assets/assetUrls'
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Override the default react components used by the editor and UI. Set components to null to
|
||||
* disable them entirely.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import {Tldraw, TLComponents} from 'tldraw'
|
||||
*
|
||||
* const components: TLComponents = {
|
||||
* Scribble: MyCustomScribble,
|
||||
* }
|
||||
*
|
||||
* export function MyApp() {
|
||||
* return <Tldraw components={components} />
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface TLComponents extends TLEditorComponents, TLUiComponents {}
|
||||
|
||||
/** @public */
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
# tldraw on cloudflare durable objects
|
||||
# tldraw sync server
|
||||
|
||||
This repo contains a complete example of tldraw sync ready to deploy on cloudflare.
|
||||
This is a production-ready backend for [tldraw sync](https://tldraw.dev/docs/sync).
|
||||
|
||||
- The client itself can be deployed wherever you like.
|
||||
- The server is on [Cloudflare Workers](https://developers.cloudflare.com/workers/).
|
||||
- Your client-side tldraw-based app can be served from anywhere you want.
|
||||
- This backend uses [Cloudflare Workers](https://developers.cloudflare.com/workers/), and will need
|
||||
to be deployed to your own Cloudflare account.
|
||||
- Each whiteboard is synced via
|
||||
[WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) to a [Cloudflare
|
||||
Durable Object](https://developers.cloudflare.com/durable-objects/).
|
||||
- Whiteboards and any uploaded images/videos are stored in a [Cloudflare
|
||||
R2](https://developers.cloudflare.com/r2/) bucket.
|
||||
- URL metadata unfurling for bookmarks shapes is also supported.
|
||||
- Although unreliated to tldraw sync, this server also includes a component to fetch link previews
|
||||
for URLs added to the canvas.
|
||||
|
||||
This is a minimal setup of the same system that powers multiplayer collaboration for hundreds of
|
||||
thousands of rooms & users on www.tldraw.com. Because durable objects effectively create a mini
|
||||
|
@ -72,3 +74,10 @@ Finally, deploy your client HTML & JavaScript. Create a production build with
|
|||
|
||||
When you visit your published client, it should connect to your cloudflare workers domain and sync
|
||||
your document across devices.
|
||||
|
||||
## License
|
||||
|
||||
Whilst the code in this template is available under the MIT license, `tldraw` and `@tldraw/sync` are
|
||||
under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md) which does not
|
||||
allow their use for commercial purposes. To purchase an alternative license for commercial use,
|
||||
contact [sales@tldraw.com](mailto:sales@tldraw.com).
|
||||
|
|
|
@ -6,3 +6,10 @@
|
|||
</div>
|
||||
|
||||
This repo contains a very basic example of how to use [tldraw](https://github.com/tldraw/tldraw) in a [Next.js](https://nextjs.org/) app using the src directory and App router.
|
||||
|
||||
## License
|
||||
|
||||
Whilst the code in this template is available under the MIT license, `tldraw` and `@tldraw/sync` are
|
||||
under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md) which does not
|
||||
allow their use for commercial purposes. To purchase an alternative license for commercial use,
|
||||
contact [sales@tldraw.com](mailto:sales@tldraw.com).
|
||||
|
|
|
@ -6,3 +6,10 @@
|
|||
</div>
|
||||
|
||||
This repo contains a template you can copy for using [tldraw](https://github.com/tldraw/tldraw) in a vite application.
|
||||
|
||||
## License
|
||||
|
||||
Whilst the code in this template is available under the MIT license, `tldraw` and `@tldraw/sync` are
|
||||
under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md) which does not
|
||||
allow their use for commercial purposes. To purchase an alternative license for commercial use,
|
||||
contact [sales@tldraw.com](mailto:sales@tldraw.com).
|
||||
|
|
Ładowanie…
Reference in New Issue