[refactor] User-facing APIs (#1478)

This PR updates our user-facing APIs for the Tldraw and TldrawEditor
components, as well as the Editor (App). It mainly incorporates surface
changes from #1450 without any changes to validators or migrators,
incorporating feedback / discussion with @SomeHats and @ds300.

Here we:
- remove the TldrawEditorConfig
- bring back a loose version of shape definitions
- make a separation between "core" shapes and "default" shapes
- do not allow custom shapes, migrators or validators to overwrite core
shapes
- but _do_ allow new shapes

## `<Tldraw>` component

In this PR, the `Tldraw` component wraps both the `TldrawEditor`
component and our `TldrawUi` component. It accepts a union of props for
both components. Previously, this component also added local syncing via
a `useLocalSyncClient` hook call, however that has been pushed down to
the `TldrawEditor` component.

## `<TldrawEditor>` component

The `TldrawEditor` component now more neatly wraps up the different ways
that the editor can be configured.

## The store prop (`TldrawEditorProps.store`)

There are three main ways for the `TldrawEditor` component to be run:
1. with an externally defined store
2. with an externally defined syncing store (local or remote)
3. with an internally defined store
4. with an internally defined locally syncing store

The `store` prop allows for these configurations.

If the `store` prop is defined, it may be defined either as a `TLStore`
or as a `SyncedStore`. If the store is a `TLStore`, then the Editor will
assume that the store is ready to go; if it is defined as a SyncedStore,
then the component will display the loading / error screens as needed,
or the final editor once the store's status is "synced".

When the store is left undefined, then the `TldrawEditor` will create
its own internal store using the optional `instanceId`, `initialData`,
or `shapes` props to define the store / store schema.

If the `persistenceKey` prop is left undefined, then the store will not
be synced. If the `persistenceKey` is defined, then the store will be
synced locally. In the future, we may also here accept the API key /
roomId / etc for creating a remotely synced store.

The `SyncedStore` type has been expanded to also include types used for
remote syncing, e.g. with `ConnectionStatus`.

## Tools

By default, the App has two "baked-in" tools: the select tool and the
zoom tool. These cannot (for now) be replaced or removed. The default
tools are used by default, but may be replaced by other tools if
provided.

## Shapes

By default, the App has a set of "core" shapes:
- group
- embed
- bookmark
- image
- video
- text

That cannot by overwritten because they're created by the app at
different moments, such as when double clicking on the canvas or via a
copy and paste event. In follow up PRs, we'll split these out so that
users can replace parts of the code where these shapes are created.

### Change Type

- [x] `major` — Breaking Change

### Test Plan

- [x] Unit Tests
pull/1497/head
Steve Ruiz 2023-06-01 16:47:34 +01:00 zatwierdzone przez GitHub
rodzic d6085e4ea6
commit 0c4174c0b8
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
87 zmienionych plików z 1430 dodań i 1563 usunięć

Wyświetl plik

@ -15,4 +15,4 @@ keywords:
Coming soon.
See the [tldraw repository](https://github.com/tldraw/tldraw) for an example of how to use the `@tldraw/tlsync-client` library to persist and sync between tabs.
See the [tldraw repository](https://github.com/tldraw/tldraw) for an example of how to use persistence with the `@tldraw/tldraw` or `@tldraw/editor` libraries.

Wyświetl plik

@ -64,7 +64,6 @@
"@tldraw/tldraw": "workspace:*",
"@tldraw/tlschema": "workspace:*",
"@tldraw/tlstore": "workspace:*",
"@tldraw/tlsync-client": "workspace:*",
"@tldraw/tlvalidate": "workspace:*",
"@tldraw/ui": "workspace:*",
"lazyrepo": "0.0.0-alpha.26",

Wyświetl plik

@ -33,7 +33,7 @@ export async function cleanup({ page }: PlaywrightTestArgs) {
}
export async function setupPage(page: PlaywrightTestArgs['page']) {
await page.goto('http://localhost:5420/e2e')
await page.goto('http://localhost:5420/end-to-end')
await page.waitForSelector('.tl-canvas')
await page.evaluate(() => (app.enableAnimations = false))
}

Wyświetl plik

@ -0,0 +1,73 @@
import test from '@playwright/test'
test.describe('Routes', () => {
test('end-to-end', async ({ page }) => {
await page.goto('http://localhost:5420/end-to-end')
await page.waitForSelector('.tl-canvas')
})
test('basic', async ({ page }) => {
await page.goto('http://localhost:5420/')
await page.waitForSelector('.tl-canvas')
})
test('api', async ({ page }) => {
await page.goto('http://localhost:5420/api')
await page.waitForSelector('.tl-canvas')
})
test('hide-ui', async ({ page }) => {
await page.goto('http://localhost:5420/custom-config')
await page.waitForSelector('.tl-canvas')
})
test('custom-config', async ({ page }) => {
await page.goto('http://localhost:5420/custom-config')
await page.waitForSelector('.tl-canvas')
})
test('custom-ui', async ({ page }) => {
await page.goto('http://localhost:5420/custom-ui')
await page.waitForSelector('.tl-canvas')
})
test('exploded', async ({ page }) => {
await page.goto('http://localhost:5420/exploded')
await page.waitForSelector('.tl-canvas')
})
test('scroll', async ({ page }) => {
await page.goto('http://localhost:5420/scroll')
await page.waitForSelector('.tl-canvas')
})
test('multiple', async ({ page }) => {
await page.goto('http://localhost:5420/multiple')
await page.waitForSelector('.tl-canvas')
})
test('error-boundary', async ({ page }) => {
await page.goto('http://localhost:5420/error-boundary')
await page.waitForSelector('.tl-canvas')
})
test('user-presence', async ({ page }) => {
await page.goto('http://localhost:5420/user-presence')
await page.waitForSelector('.tl-canvas')
})
test('ui-events', async ({ page }) => {
await page.goto('http://localhost:5420/ui-events')
await page.waitForSelector('.tl-canvas')
})
test('store-events', async ({ page }) => {
await page.goto('http://localhost:5420/store-events')
await page.waitForSelector('.tl-canvas')
})
test('persistence', async ({ page }) => {
await page.goto('http://localhost:5420/persistence')
await page.waitForSelector('.tl-canvas')
})
})

Wyświetl plik

@ -1,37 +1,21 @@
import {
Canvas,
ContextMenu,
TAB_ID,
TldrawEditor,
TldrawEditorConfig,
TldrawUi,
} from '@tldraw/tldraw'
import { Canvas, ContextMenu, TAB_ID, TldrawEditor, TldrawUi, createTLStore } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
import { throttle } from '@tldraw/utils'
import { useEffect, useState } from 'react'
import { useLayoutEffect, useState } from 'react'
const PERSISTENCE_KEY = 'example-3'
const config = new TldrawEditorConfig()
const instanceId = TAB_ID
const store = config.createStore({ instanceId })
export default function PersistenceExample() {
const [state, setState] = useState<
| {
name: 'loading'
}
| {
name: 'ready'
}
| {
name: 'error'
error: string
}
>({ name: 'loading', error: undefined })
const [store] = useState(() => createTLStore({ instanceId: TAB_ID }))
const [loadingStore, setLoadingStore] = useState<
{ status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string }
>({
status: 'loading',
})
useEffect(() => {
setState({ name: 'loading' })
useLayoutEffect(() => {
setLoadingStore({ status: 'loading' })
// Get persisted data from local storage
const persistedSnapshot = localStorage.getItem(PERSISTENCE_KEY)
@ -40,29 +24,28 @@ export default function PersistenceExample() {
try {
const snapshot = JSON.parse(persistedSnapshot)
store.loadSnapshot(snapshot)
setState({ name: 'ready' })
} catch (e: any) {
setState({ name: 'error', error: e.message }) // Something went wrong
setLoadingStore({ status: 'ready' })
} catch (error: any) {
setLoadingStore({ status: 'error', error: error.message }) // Something went wrong
}
} else {
setState({ name: 'ready' }) // Nothing persisted, continue with the empty store
setLoadingStore({ status: 'ready' }) // Nothing persisted, continue with the empty store
}
const persist = throttle(() => {
// Each time the store changes, persist the store snapshot
const snapshot = store.getSnapshot()
localStorage.setItem(PERSISTENCE_KEY, JSON.stringify(snapshot))
}, 1000)
// Each time the store changes, run the (debounced) persist function
const cleanupFn = store.listen(persist)
const cleanupFn = store.listen(
throttle(() => {
const snapshot = store.getSnapshot()
localStorage.setItem(PERSISTENCE_KEY, JSON.stringify(snapshot))
}, 500)
)
return () => {
cleanupFn()
}
}, [])
}, [store])
if (state.name === 'loading') {
if (loadingStore.status === 'loading') {
return (
<div className="tldraw__editor">
<h2>Loading...</h2>
@ -70,18 +53,18 @@ export default function PersistenceExample() {
)
}
if (state.name === 'error') {
if (loadingStore.status === 'error') {
return (
<div className="tldraw__editor">
<h2>Error!</h2>
<p>{state.error}</p>
<p>{loadingStore.error}</p>
</div>
)
}
return (
<div className="tldraw__editor">
<TldrawEditor instanceId={instanceId} store={store} config={config} autoFocus>
<TldrawEditor store={store} autoFocus>
<TldrawUi>
<ContextMenu>
<Canvas />

Wyświetl plik

@ -10,7 +10,7 @@ import { useEffect } from 'react'
// component and all shapes, tools, and UI components use this instance to
// send events, observe changes, and perform actions.
export default function Example() {
export default function APIExample() {
const handleMount = (app: App) => {
// Create a shape id
const id = app.createShapeId('hello')

Wyświetl plik

@ -0,0 +1,10 @@
import { TLBaseShape, TLOpacityType } from '@tldraw/tldraw'
export type CardShape = TLBaseShape<
'card',
{
opacity: TLOpacityType // necessary for all shapes at the moment, others can be whatever you want!
w: number
h: number
}
>

Wyświetl plik

@ -0,0 +1,13 @@
// Tool
// ----
// Because the card tool can be just a rectangle, we can extend the
import { TLBoxTool } from '@tldraw/tldraw'
// TLBoxTool class. This gives us a lot of functionality for free.
export class CardTool extends TLBoxTool {
static override id = 'card'
static override initial = 'idle'
override shapeType = 'card'
}

Wyświetl plik

@ -0,0 +1,46 @@
import { HTMLContainer, TLBoxUtil } from '@tldraw/tldraw'
import { CardShape } from './CardShape'
export class CardUtil extends TLBoxUtil<CardShape> {
// Id — the shape util's id
static override type = 'card' as const
// Flags — there are a LOT of other flags!
override isAspectRatioLocked = (_shape: CardShape) => false
override canResize = (_shape: CardShape) => true
override canBind = (_shape: CardShape) => true
// Default props — used for shapes created with the tool
override defaultProps(): CardShape['props'] {
return {
opacity: '1',
w: 300,
h: 300,
}
}
// Render method — the React component that will be rendered for the shape
render(shape: CardShape) {
const bounds = this.bounds(shape)
return (
<HTMLContainer
id={shape.id}
style={{
border: '1px solid black',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'all',
}}
>
{bounds.w.toFixed()}x{bounds.h.toFixed()}
</HTMLContainer>
)
}
// Indicator — used when hovering over a shape or when it's selected; must return only SVG elements here
indicator(shape: CardShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
}

Wyświetl plik

@ -1,120 +1,25 @@
import {
HTMLContainer,
MenuGroup,
menuItem,
TLBaseShape,
TLBoxTool,
TLBoxUtil,
Tldraw,
TldrawEditorConfig,
TLOpacityType,
toolbarItem,
} from '@tldraw/tldraw'
import { MenuGroup, Tldraw, menuItem, toolbarItem } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
import { CardTool } from './CardTool'
import { CardUtil } from './CardUtil'
// Let's make a custom shape called a Card.
const shapes = { card: { util: CardUtil } }
const tools = [CardTool]
// Shape Type
// ----------
// The shape type defines the card's type (`card`) and its props.
// Every shape needs an opacity prop (for now), but other than that
// you can add whatever you want, so long as it's JSON serializable.
type CardShape = TLBaseShape<
'card',
{
w: number
h: number
opacity: TLOpacityType
}
>
// Shape Util
// ----------
// The CardUtil class is used by the app to answer questions about a
// shape of the 'card' type. For example, what is the default props
// for this shape? What should we render for it, or for its indicator?
class CardUtil extends TLBoxUtil<CardShape> {
static override type = 'card' as const
// There are a LOT of other things we could add here, like these flags
override isAspectRatioLocked = (_shape: CardShape) => false
override canResize = (_shape: CardShape) => true
override canBind = (_shape: CardShape) => true
override defaultProps(): CardShape['props'] {
return {
opacity: '1',
w: 300,
h: 300,
}
}
// This is the component that will be rendered for the shape.
// Try changing the contents of the HTMLContainer to see what happens.
render(shape: CardShape) {
// You can access class methods from here
const bounds = this.bounds(shape)
return (
<HTMLContainer
id={shape.id}
style={{
border: '1px solid black',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'all',
}}
>
{/* Anything you want can go here—it's a regular React component */}
{bounds.w.toFixed()}x{bounds.h.toFixed()}
</HTMLContainer>
)
}
// The indicator is used when hovering over a shape or when it's selected.
// This can only be SVG path data; generally you want the outline of the
// component you're rendering.
indicator(shape: CardShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
}
// Tool
// ----
// Because the card tool can be just a rectangle, we can extend the
// TLBoxTool class. This gives us a lot of functionality for free.
export class CardTool extends TLBoxTool {
static override id = 'card'
static override initial = 'idle'
override shapeType = 'card'
}
// Finally, collect the custom tools and shapes into a config object
const customTldrawConfig = new TldrawEditorConfig({
tools: [CardTool],
shapes: {
card: {
util: CardUtil,
},
},
})
// ... and we can make our custom shape example!
export default function Example() {
export default function CustomConfigExample() {
return (
<div className="tldraw__editor">
<Tldraw
persistenceKey="custom-config"
config={customTldrawConfig}
autoFocus
tools={tools}
shapes={shapes}
overrides={{
// In order for our custom tool to show up in the UI...
// We need to add it to the tools list. This "toolItem"
// has information about its icon, label, keyboard shortcut,
// and what to do when it's selected.
tools(app, tools) {
// In order for our custom tool to show up in the UI...
// We need to add it to the tools list. This "toolItem"
// has information about its icon, label, keyboard shortcut,
// and what to do when it's selected.
tools.card = {
id: 'card',
icon: 'color',
@ -127,13 +32,13 @@ export default function Example() {
}
return tools
},
toolbar(app, toolbar, { tools }) {
toolbar(_app, toolbar, { tools }) {
// The toolbar is an array of items. We can add it to the
// end of the array or splice it in, then return the array.
toolbar.splice(4, 0, toolbarItem(tools.card))
return toolbar
},
keyboardShortcutsMenu(app, keyboardShortcutsMenu, { tools }) {
keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
// Same for the keyboard shortcuts menu, but this menu contains
// both items and groups. We want to find the "Tools" group and
// add it to that before returning the array.

Wyświetl plik

@ -1,15 +1,13 @@
import { Canvas, TldrawEditor, TldrawEditorConfig, useApp } from '@tldraw/tldraw'
import { Canvas, TldrawEditor, useApp } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import { useEffect } from 'react'
import { track } from 'signia-react'
import './custom-ui.css'
const config = new TldrawEditorConfig()
export default function Example() {
export default function CustomUiExample() {
return (
<div className="tldraw__editor">
<TldrawEditor config={config} autoFocus>
<TldrawEditor autoFocus>
<Canvas />
<CustomUi />
</TldrawEditor>

Wyświetl plik

@ -1,30 +1,11 @@
import {
Canvas,
ContextMenu,
InstanceRecordType,
TldrawEditor,
TldrawEditorConfig,
TldrawUi,
useLocalSyncClient,
} from '@tldraw/tldraw'
import { Canvas, ContextMenu, TldrawEditor, TldrawUi } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
const instanceId = InstanceRecordType.createCustomId('example')
// for custom config, see 3-custom-config
const config = new TldrawEditorConfig()
export default function Example() {
const syncedStore = useLocalSyncClient({
config,
instanceId,
universalPersistenceKey: 'exploded-example',
})
export default function ExplodedExample() {
return (
<div className="tldraw__editor">
<TldrawEditor instanceId={instanceId} store={syncedStore} config={config} autoFocus>
<TldrawEditor autoFocus persistenceKey="exploded-example">
<TldrawUi>
<ContextMenu>
<Canvas />

Wyświetl plik

@ -2,7 +2,7 @@ import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
export default function Example() {
export default function MultipleExample() {
return (
<div
style={{

Wyświetl plik

@ -1,64 +0,0 @@
import { createShapeId, TLBaseShape, TLBoxUtil, Tldraw, TldrawEditorConfig } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
export default function ErrorBoundaryExample() {
return (
<div className="tldraw__editor">
<Tldraw
components={{
// disable app-level error boundaries:
ErrorFallback: null,
// use a custom error fallback for shapes:
ShapeErrorFallback: ({ error }) => <div>Shape error! {String(error)}</div>,
}}
// below, we define a custom shape that always throws an error so we can see our new error boundary in action
config={customConfigWithErrorShape}
onMount={(app) => {
// when the app starts, create our error shape so we can see
// what it looks like:
app.createShapes([
{
type: 'error',
id: createShapeId(),
x: 0,
y: 0,
props: { message: 'Something has gone wrong' },
},
])
// center the camera on the error shape
app.zoomToFit()
app.resetZoom()
}}
/>
</div>
)
}
// do make it easy to see our custom shape error fallback, let's create a new
// shape type that always throws an error. See CustomConfigExample for more info
// on creating custom shapes.
type ErrorShape = TLBaseShape<'error', { w: number; h: number; message: string }>
class ErrorUtil extends TLBoxUtil<ErrorShape> {
override type = 'error' as const
defaultProps() {
return { message: 'Error!', w: 100, h: 100 }
}
render(shape: ErrorShape) {
throw new Error(shape.props.message)
}
indicator() {
throw new Error(`Error shape indicator!`)
}
}
const customConfigWithErrorShape = new TldrawEditorConfig({
shapes: {
error: {
util: ErrorUtil,
},
},
})

Wyświetl plik

@ -0,0 +1,40 @@
import { createShapeId, Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
import { ErrorUtil } from './ErrorUtil'
const shapes = {
error: {
util: ErrorUtil, // a custom shape that will always error
},
}
export default function ErrorBoundaryExample() {
return (
<div className="tldraw__editor">
<Tldraw
shapes={shapes}
components={{
ErrorFallback: null, // disable app-level error boundaries
ShapeErrorFallback: ({ error }) => <div>Shape error! {String(error)}</div>, // use a custom error fallback for shapes
}}
onMount={(app) => {
// When the app starts, create our error shape so we can see.
app.createShapes([
{
type: 'error',
id: createShapeId(),
x: 0,
y: 0,
props: { message: 'Something has gone wrong' },
},
])
// Center the camera on the error shape
app.zoomToFit()
app.resetZoom()
}}
/>
</div>
)
}

Wyświetl plik

@ -0,0 +1,3 @@
import { TLBaseShape } from '@tldraw/tldraw'
export type ErrorShape = TLBaseShape<'error', { w: number; h: number; message: string }>

Wyświetl plik

@ -0,0 +1,17 @@
import { TLBoxUtil } from '@tldraw/tldraw'
import { ErrorShape } from './ErrorShape'
export class ErrorUtil extends TLBoxUtil<ErrorShape> {
static override type = 'error'
override type = 'error' as const
defaultProps() {
return { message: 'Error!', w: 100, h: 100 }
}
render(shape: ErrorShape) {
throw new Error(shape.props.message)
}
indicator() {
throw new Error(`Error shape indicator!`)
}
}

Wyświetl plik

@ -2,9 +2,8 @@ import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
export default function ForEndToEndTests() {
export default function EndToEnd() {
;(window as any).__tldraw_editor_events = []
return (
<div className="tldraw__editor">
<Tldraw

Wyświetl plik

@ -12,7 +12,7 @@ import ExampleBasic from './1-basic/BasicExample'
import CustomComponentsExample from './10-custom-components/CustomComponentsExample'
import UserPresenceExample from './11-user-presence/UserPresenceExample'
import UiEventsExample from './12-ui-events/UiEventsExample'
import StoreEventsExample from './13-store/StoreEventsExample'
import StoreEventsExample from './13-store-events/StoreEventsExample'
import PersistenceExample from './14-persistence/PersistenceExample'
import ExampleApi from './2-api/APIExample'
import CustomConfigExample from './3-custom-config/CustomConfigExample'
@ -20,9 +20,11 @@ import CustomUiExample from './4-custom-ui/CustomUiExample'
import ExplodedExample from './5-exploded/ExplodedExample'
import ExampleScroll from './6-scroll/ScrollExample'
import ExampleMultiple from './7-multiple/MultipleExample'
import ErrorBoundaryExample from './8-error-boundaries/ErrorBoundaryExample'
import ErrorBoundaryExample from './8-error-boundary/ErrorBoundaryExample'
import HideUiExample from './9-hide-ui/HideUiExample'
import ForEndToEndTests from './end-to-end/ForEndToEndTests'
import EndToEnd from './end-to-end/end-to-end'
// This example is only used for end to end tests
// we use secret internal `setDefaultAssetUrls` functions to set these at the
// top-level so assets don't need to be passed down in every single example.
@ -34,6 +36,7 @@ type Example = {
path: string
element: JSX.Element
}
export const allExamples: Example[] = [
{
path: '/',
@ -52,7 +55,7 @@ export const allExamples: Example[] = [
element: <ExampleApi />,
},
{
path: '/custom',
path: '/custom-config',
element: <CustomConfigExample />,
},
{
@ -92,8 +95,8 @@ export const allExamples: Example[] = [
element: <PersistenceExample />,
},
{
path: '/e2e',
element: <ForEndToEndTests />,
path: '/end-to-end',
element: <EndToEnd />,
},
]

Wyświetl plik

@ -37,7 +37,6 @@
"@tldraw/editor": "workspace:*",
"@tldraw/file-format": "workspace:*",
"@tldraw/tldraw": "workspace:*",
"@tldraw/tlsync-client": "workspace:*",
"@tldraw/ui": "workspace:*",
"@tldraw/utils": "workspace:*",
"@types/fs-extra": "^11.0.1",

Wyświetl plik

@ -1,4 +1,4 @@
import { SyncedStore, TLInstanceId, useApp } from '@tldraw/editor'
import { useApp } from '@tldraw/editor'
import { parseAndLoadDocument, serializeTldrawJson } from '@tldraw/file-format'
import { useDefaultHelpers } from '@tldraw/ui'
import { debounce } from '@tldraw/utils'
@ -9,13 +9,7 @@ import { vscode } from './utils/vscode'
// @ts-ignore
import type { VscodeMessage } from '../../messages'
export const ChangeResponder = ({
syncedStore,
instanceId,
}: {
syncedStore: SyncedStore
instanceId: TLInstanceId
}) => {
export const ChangeResponder = () => {
const app = useApp()
const { addToast, clearToasts, msg } = useDefaultHelpers()
@ -44,19 +38,17 @@ export const ChangeResponder = ({
clearToasts()
window.removeEventListener('message', handleMessage)
}
}, [app, instanceId, msg, addToast, clearToasts])
}, [app, msg, addToast, clearToasts])
React.useEffect(() => {
// When the history changes, send the new file contents to VSCode
const handleChange = debounce(async () => {
if (syncedStore.store) {
vscode.postMessage({
type: 'vscode:editor-updated',
data: {
fileContents: await serializeTldrawJson(syncedStore.store),
},
})
}
vscode.postMessage({
type: 'vscode:editor-updated',
data: {
fileContents: await serializeTldrawJson(app.store),
},
})
}, 250)
vscode.postMessage({
@ -69,7 +61,7 @@ export const ChangeResponder = ({
handleChange()
app.off('change-history', handleChange)
}
}, [app, syncedStore, instanceId])
}, [app])
return null
}

Wyświetl plik

@ -1,4 +1,4 @@
import { TLInstanceId, useApp } from '@tldraw/editor'
import { useApp } from '@tldraw/editor'
import { parseAndLoadDocument } from '@tldraw/file-format'
import { useDefaultHelpers } from '@tldraw/ui'
import React from 'react'
@ -6,10 +6,8 @@ import { vscode } from './utils/vscode'
export function FileOpen({
fileContents,
instanceId,
forceDarkMode,
}: {
instanceId: TLInstanceId
fileContents: string
forceDarkMode: boolean
}) {
@ -42,7 +40,7 @@ export function FileOpen({
return () => {
clearToasts()
}
}, [fileContents, app, instanceId, addToast, msg, clearToasts, forceDarkMode, isFileLoaded])
}, [fileContents, app, addToast, msg, clearToasts, forceDarkMode, isFileLoaded])
return null
}

Wyświetl plik

@ -2,19 +2,18 @@ import {
App,
Canvas,
ErrorBoundary,
setRuntimeOverrides,
TAB_ID,
TldrawEditor,
TldrawEditorConfig,
setRuntimeOverrides,
} from '@tldraw/editor'
import { linksUiOverrides } from './utils/links'
// eslint-disable-next-line import/no-internal-modules
import '@tldraw/editor/editor.css'
import { TAB_ID, useLocalSyncClient } from '@tldraw/tlsync-client'
import { ContextMenu, MenuSchema, TldrawUi } from '@tldraw/ui'
// eslint-disable-next-line import/no-internal-modules
import { getAssetUrlsByImport } from '@tldraw/assets/imports'
// eslint-disable-next-line import/no-internal-modules
import '@tldraw/ui/ui.css'
// eslint-disable-next-line import/no-internal-modules
import { getAssetUrlsByImport } from '@tldraw/assets/imports'
import { useEffect, useMemo, useState } from 'react'
import { VscodeMessage } from '../../messages'
import '../public/index.css'
@ -24,10 +23,6 @@ import { FullPageMessage } from './FullPageMessage'
import { onCreateBookmarkFromUrl } from './utils/bookmarks'
import { vscode } from './utils/vscode'
const config = new TldrawEditorConfig()
// @ts-ignore
setRuntimeOverrides({
openWindow: (url, target) => {
vscode.postMessage({
@ -97,7 +92,6 @@ export const TldrawWrapper = () => {
fileContents: message.data.fileContents,
uri: message.data.uri,
isDarkMode: message.data.isDarkMode,
config,
})
// We only want to listen for this message once
window.removeEventListener('message', handleMessage)
@ -127,32 +121,23 @@ export type TLDrawInnerProps = {
fileContents: string
uri: string
isDarkMode: boolean
config: TldrawEditorConfig
}
function TldrawInner({ uri, config, assetSrc, isDarkMode, fileContents }: TLDrawInnerProps) {
const instanceId = TAB_ID
const syncedStore = useLocalSyncClient({
universalPersistenceKey: uri,
instanceId,
config,
})
function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerProps) {
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
return (
<TldrawEditor
config={config}
assetUrls={assetUrls}
instanceId={TAB_ID}
store={syncedStore}
persistenceKey={uri}
onCreateBookmarkFromUrl={onCreateBookmarkFromUrl}
autoFocus
>
{/* <DarkModeHandler themeKind={themeKind} /> */}
<TldrawUi assetUrls={assetUrls} overrides={[menuOverrides, linksUiOverrides]}>
<FileOpen instanceId={instanceId} fileContents={fileContents} forceDarkMode={isDarkMode} />
<ChangeResponder syncedStore={syncedStore} instanceId={instanceId} />
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
<ChangeResponder />
<ContextMenu>
<Canvas />
</ContextMenu>

Wyświetl plik

@ -29,7 +29,6 @@
{ "path": "../../../packages/file-format" },
{ "path": "../../../packages/ui" },
{ "path": "../../../packages/editor" },
{ "path": "../../../packages/tlsync-client" },
{ "path": "../../../packages/utils" }
]
}

Wyświetl plik

@ -124,7 +124,6 @@
"scripts": {
"dev": "tsx scripts/dev.ts",
"build": "cd ../editor && yarn build && cd ../extension && tsx scripts/build.ts",
"web": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=.",
"package": "yarn build && tsx scripts/package.ts",
"publish": "vsce publish",
"lint": "yarn run -T tsx ../../../scripts/lint.ts",

Wyświetl plik

@ -1,16 +1,16 @@
import { TldrawEditorConfig } from '@tldraw/editor'
import { createTLSchema } from '@tldraw/editor'
import { TldrawFile } from '@tldraw/file-format'
import * as vscode from 'vscode'
export const defaultFileContents: TldrawFile = {
tldrawFileFormatVersion: 1,
schema: new TldrawEditorConfig().storeSchema.serialize(),
schema: createTLSchema().serialize(),
records: [],
}
export const fileContentWithErrors: TldrawFile = {
tldrawFileFormatVersion: 1,
schema: new TldrawEditorConfig().storeSchema.serialize(),
schema: createTLSchema().serialize(),
records: [{ typeName: 'shape', id: null } as any],
}

Wyświetl plik

@ -29,9 +29,8 @@ import { Matrix2d } from '@tldraw/primitives';
import { Matrix2dModel } from '@tldraw/primitives';
import { Migrations } from '@tldraw/tlstore';
import { Polyline2d } from '@tldraw/primitives';
import * as React_2 from 'react';
import { default as React_3 } from 'react';
import { RecordType } from '@tldraw/tlstore';
import { default as React_2 } from 'react';
import * as React_3 from 'react';
import { RotateCorner } from '@tldraw/primitives';
import { SelectionCorner } from '@tldraw/primitives';
import { SelectionEdge } from '@tldraw/primitives';
@ -39,7 +38,6 @@ import { SelectionHandle } from '@tldraw/primitives';
import { SerializedSchema } from '@tldraw/tlstore';
import { Signal } from 'signia';
import { sortByIndex } from '@tldraw/indices';
import { StoreSchema } from '@tldraw/tlstore';
import { StoreSnapshot } from '@tldraw/tlstore';
import { StrokePoint } from '@tldraw/primitives';
import { TLAlignType } from '@tldraw/tlschema';
@ -57,7 +55,6 @@ import { TLColorType } from '@tldraw/tlschema';
import { TLCursor } from '@tldraw/tlschema';
import { TLDocument } from '@tldraw/tlschema';
import { TLDrawShape } from '@tldraw/tlschema';
import { TLDrawShapeSegment } from '@tldraw/tlschema';
import { TLEmbedShape } from '@tldraw/tlschema';
import { TLFontType } from '@tldraw/tlschema';
import { TLFrameShape } from '@tldraw/tlschema';
@ -88,7 +85,6 @@ import { TLShapeProps } from '@tldraw/tlschema';
import { TLSizeStyle } from '@tldraw/tlschema';
import { TLSizeType } from '@tldraw/tlschema';
import { TLStore } from '@tldraw/tlschema';
import { TLStoreProps } from '@tldraw/tlschema';
import { TLStyleCollections } from '@tldraw/tlschema';
import { TLStyleType } from '@tldraw/tlschema';
import { TLTextShape } from '@tldraw/tlschema';
@ -125,7 +121,7 @@ export type AnimationOptions = Partial<{
// @public (undocumented)
export class App extends EventEmitter<TLEventMap> {
constructor({ config, store, getContainer }: AppOptions);
constructor({ store, user, tools, shapes, getContainer, }: AppOptions);
addOpenMenu: (id: string) => this;
alignShapes(operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top', ids?: TLShapeId[]): this;
get allShapesCommonBounds(): Box2d | null;
@ -167,7 +163,6 @@ export class App extends EventEmitter<TLEventMap> {
// @internal
protected _clickManager: ClickManager;
complete(): this;
readonly config: TldrawEditorConfig;
// @internal (undocumented)
crash(error: unknown): void;
// @internal
@ -551,9 +546,11 @@ export function applyRotationToSnapshotShapes({ delta, app, snapshot, stage, }:
// @public (undocumented)
export interface AppOptions {
config: TldrawEditorConfig;
getContainer: () => HTMLElement;
shapes?: Record<string, ShapeInfo>;
store: TLStore;
tools?: StateNodeConstructor[];
user?: TLUser;
}
// @public (undocumented)
@ -569,8 +566,8 @@ export const BOUND_ARROW_OFFSET = 10;
export function buildFromV1Document(app: App, document: LegacyTldrawDocument): void;
// @public (undocumented)
export const Canvas: React_2.MemoExoticComponent<({ onDropOverride, }: {
onDropOverride?: ((defaultOnDrop: (e: React_2.DragEvent<Element>) => Promise<void>) => (e: React_2.DragEvent<Element>) => Promise<void>) | undefined;
export const Canvas: React_3.MemoExoticComponent<({ onDropOverride, }: {
onDropOverride?: ((defaultOnDrop: (e: React_3.DragEvent<Element>) => Promise<void>) => (e: React_3.DragEvent<Element>) => Promise<void>) | undefined;
}) => JSX.Element>;
// @public (undocumented)
@ -613,6 +610,9 @@ export function createEmbedShapeAtPoint(app: App, url: string, point: Vec2dModel
// @public (undocumented)
export function createShapesFromFiles(app: App, files: File[], position: VecLike, _ignoreParent?: boolean): Promise<void>;
// @public
export function createTLStore(opts?: StoreOptions): TLStore;
// @public (undocumented)
export function dataTransferItemAsString(item: DataTransferItem): Promise<string>;
@ -658,6 +658,12 @@ export function defaultEmptyAs(str: string, dflt: string): string;
// @internal (undocumented)
export const DefaultErrorFallback: TLErrorFallback;
// @public (undocumented)
export const defaultShapes: Record<string, ShapeInfo>;
// @public (undocumented)
export const defaultTools: StateNodeConstructor[];
// @internal (undocumented)
export const DOUBLE_CLICK_DURATION = 450;
@ -685,7 +691,7 @@ export type EmbedResult = {
} | undefined;
// @public (undocumented)
export class ErrorBoundary extends React_2.Component<React_2.PropsWithRef<React_2.PropsWithChildren<ErrorBoundaryProps>>, ErrorBoundaryState> {
export class ErrorBoundary extends React_3.Component<React_3.PropsWithRef<React_3.PropsWithChildren<ErrorBoundaryProps>>, ErrorBoundaryState> {
// (undocumented)
componentDidCatch(error: unknown): void;
// (undocumented)
@ -693,7 +699,7 @@ export class ErrorBoundary extends React_2.Component<React_2.PropsWithRef<React_
error: Error;
};
// (undocumented)
render(): React_2.ReactNode;
render(): React_3.ReactNode;
// (undocumented)
state: ErrorBoundaryState;
}
@ -701,9 +707,9 @@ export class ErrorBoundary extends React_2.Component<React_2.PropsWithRef<React_
// @public (undocumented)
export interface ErrorBoundaryProps {
// (undocumented)
children: React_2.ReactNode;
children: React_3.ReactNode;
// (undocumented)
fallback: (error: unknown) => React_2.ReactNode;
fallback: (error: unknown) => React_3.ReactNode;
// (undocumented)
onError?: ((error: unknown) => void) | null;
}
@ -713,16 +719,6 @@ export function ErrorScreen({ children }: {
children: any;
}): JSX.Element;
// @public (undocumented)
export interface ErrorSyncedStore {
// (undocumented)
readonly error: Error;
// (undocumented)
readonly status: 'error';
// (undocumented)
readonly store?: undefined;
}
// @public (undocumented)
export const EVENT_NAME_MAP: Record<Exclude<TLEventName, TLPinchEventName>, keyof TLEventHandlers>;
@ -842,6 +838,9 @@ export function getSvgPathFromStrokePoints(points: StrokePoint[], closed?: boole
// @public (undocumented)
export function getTextBoundingBox(text: SVGTextElement): DOMRect;
// @public (undocumented)
export function getUserPreferences(): TLUserPreferences;
// @public (undocumented)
export const getValidHttpURLList: (url: string) => string[] | undefined;
@ -864,6 +863,11 @@ export const GRID_STEPS: {
// @internal (undocumented)
export const HAND_TOOL_FRICTION = 0.09;
// @public
export function hardReset({ shouldReload }?: {
shouldReload?: boolean | undefined;
}): Promise<void>;
// @public (undocumented)
export function hardResetApp(): void;
@ -874,7 +878,7 @@ export const HASH_PATERN_ZOOM_NAMES: Record<string, string>;
export function HTMLContainer({ children, className, ...rest }: HTMLContainerProps): JSX.Element;
// @public (undocumented)
export type HTMLContainerProps = React_2.HTMLAttributes<HTMLDivElement>;
export type HTMLContainerProps = React_3.HTMLAttributes<HTMLDivElement>;
// @public (undocumented)
export const ICON_SIZES: Record<TLSizeType, number>;
@ -882,16 +886,6 @@ export const ICON_SIZES: Record<TLSizeType, number>;
// @public (undocumented)
export const INDENT = " ";
// @public (undocumented)
export interface InitializingSyncedStore {
// (undocumented)
readonly error?: undefined;
// (undocumented)
readonly status: 'loading';
// (undocumented)
readonly store?: undefined;
}
// @public
export function isAnimated(buffer: ArrayBuffer): boolean;
@ -1392,27 +1386,17 @@ export function openWindow(url: string, target?: string): void;
// @internal (undocumented)
export function OptionalErrorBoundary({ children, fallback, ...props }: Omit<ErrorBoundaryProps, 'fallback'> & {
fallback: ((error: unknown) => React_2.ReactNode) | null;
fallback: ((error: unknown) => React_3.ReactNode) | null;
}): JSX.Element;
// @public
export function preventDefault(event: Event | React_3.BaseSyntheticEvent): void;
// @public (undocumented)
export interface ReadySyncedStore {
// (undocumented)
readonly error?: undefined;
// (undocumented)
readonly status: 'synced';
// (undocumented)
readonly store: TLStore;
}
export function preventDefault(event: Event | React_2.BaseSyntheticEvent): void;
// @public (undocumented)
export function refreshPage(): void;
// @public (undocumented)
export function releasePointerCapture(element: Element, event: PointerEvent | React_3.PointerEvent<Element>): void;
export function releasePointerCapture(element: Element, event: PointerEvent | React_2.PointerEvent<Element>): void;
// @internal (undocumented)
export const REMOVE_SYMBOL: unique symbol;
@ -1455,7 +1439,7 @@ export const runtime: {
export function setDefaultEditorAssetUrls(assetUrls: EditorAssetUrls): void;
// @public (undocumented)
export function setPointerCapture(element: Element, event: PointerEvent | React_3.PointerEvent<Element>): void;
export function setPointerCapture(element: Element, event: PointerEvent | React_2.PointerEvent<Element>): void;
// @public (undocumented)
export function setPropsForNextShape(previousProps: TLInstancePropsForNextShape, newProps: Partial<TLShapeProps>): TLInstancePropsForNextShape;
@ -1463,6 +1447,9 @@ export function setPropsForNextShape(previousProps: TLInstancePropsForNextShape,
// @public (undocumented)
export function setRuntimeOverrides(input: Partial<typeof runtime>): void;
// @public (undocumented)
export function setUserPreferences(user: TLUserPreferences): void;
// @public (undocumented)
export function snapToGrid(n: number, gridSize: number): number;
@ -1559,6 +1546,30 @@ export interface StateNodeConstructor {
styles?: TLStyleType[];
}
// @public (undocumented)
export type StoreWithStatus = {
readonly status: 'error';
readonly store?: undefined;
readonly error: Error;
} | {
readonly status: 'loading';
readonly store?: undefined;
readonly error?: undefined;
} | {
readonly status: 'not-synced';
readonly store: TLStore;
readonly error?: undefined;
} | {
readonly status: 'synced-local';
readonly store: TLStore;
readonly error?: undefined;
} | {
readonly status: 'synced-remote';
readonly connectionStatus: 'offline' | 'online';
readonly store: TLStore;
readonly error?: undefined;
};
// @public (undocumented)
export const STYLES: TLStyleCollections;
@ -1569,10 +1580,10 @@ export const SVG_PADDING = 32;
export function SVGContainer({ children, className, ...rest }: SVGContainerProps): JSX.Element;
// @public (undocumented)
export type SVGContainerProps = React_2.HTMLAttributes<SVGElement>;
export type SVGContainerProps = React_3.HTMLAttributes<SVGElement>;
// @public (undocumented)
export type SyncedStore = ErrorSyncedStore | InitializingSyncedStore | ReadySyncedStore;
export const TAB_ID: TLInstanceId;
// @public (undocumented)
export const TEXT_PROPS: {
@ -1696,7 +1707,7 @@ export type TLBoxLike = TLBaseShape<string, {
// @public (undocumented)
export abstract class TLBoxTool extends StateNode {
// (undocumented)
static children: () => (typeof Idle_4 | typeof Pointing_3)[];
static children: () => (typeof Idle_4 | typeof Pointing_2)[];
// (undocumented)
static id: string;
// (undocumented)
@ -1793,51 +1804,31 @@ export type TLCompleteEventInfo = {
export type TLCopyType = 'jpeg' | 'json' | 'png' | 'svg';
// @public (undocumented)
export function TldrawEditor(props: TldrawEditorProps): JSX.Element;
export const TldrawEditor: React_2.NamedExoticComponent<TldrawEditorProps>;
// @public (undocumented)
export class TldrawEditorConfig {
constructor(opts?: TldrawEditorConfigOptions);
// (undocumented)
createStore(config: {
initialData?: StoreSnapshot<TLRecord>;
instanceId: TLInstanceId;
}): TLStore;
// (undocumented)
readonly derivePresenceState: (store: TLStore) => Signal<null | TLInstancePresence>;
// (undocumented)
readonly setUserPreferences: (userPreferences: TLUserPreferences) => void;
// (undocumented)
readonly shapeUtils: Record<TLShape['type'], TLShapeUtilConstructor<any>>;
// (undocumented)
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>;
// (undocumented)
readonly TLShape: RecordType<TLShape, 'index' | 'parentId' | 'props' | 'type'>;
// (undocumented)
readonly tools: readonly StateNodeConstructor[];
// (undocumented)
readonly userPreferences: Signal<TLUserPreferences>;
}
// @public (undocumented)
export interface TldrawEditorProps {
export type TldrawEditorProps = {
children?: any;
shapes?: Record<string, ShapeInfo>;
tools?: StateNodeConstructor[];
assetUrls?: EditorAssetUrls;
autoFocus?: boolean;
// (undocumented)
children?: any;
components?: Partial<TLEditorComponents>;
config: TldrawEditorConfig;
instanceId?: TLInstanceId;
isDarkMode?: boolean;
onMount?: (app: App) => void;
onCreateAssetFromFile?: (file: File) => Promise<TLAsset>;
onCreateBookmarkFromUrl?: (url: string) => Promise<{
image: string;
title: string;
description: string;
}>;
onMount?: (app: App) => void;
store?: SyncedStore | TLStore;
}
} & ({
store: StoreWithStatus | TLStore;
} | {
store?: undefined;
initialData?: StoreSnapshot<TLRecord>;
instanceId?: TLInstanceId;
persistenceKey?: string;
});
// @public (undocumented)
export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
@ -2209,6 +2200,8 @@ export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
render(shape: TLGroupShape): JSX.Element | null;
// (undocumented)
static type: string;
// (undocumented)
type: "group";
}
// @public (undocumented)
@ -2570,6 +2563,8 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
export interface TLShapeUtilConstructor<T extends TLUnknownShape, ShapeUtil extends TLShapeUtil<T> = TLShapeUtil<T>> {
// (undocumented)
new (app: App, type: T['type']): ShapeUtil;
// (undocumented)
type: T['type'];
}
// @public (undocumented)
@ -2663,6 +2658,22 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
// @public (undocumented)
export type TLTickEvent = (elapsed: number) => void;
// @public
export interface TLUserPreferences {
// (undocumented)
animationSpeed: number;
// (undocumented)
color: string;
// (undocumented)
id: string;
// (undocumented)
isDarkMode: boolean;
// (undocumented)
locale: string;
// (undocumented)
name: string;
}
// @public (undocumented)
export class TLVideoUtil extends TLBoxUtil<TLVideoShape> {
// (undocumented)
@ -2715,6 +2726,11 @@ export const useApp: () => App;
// @public (undocumented)
export function useContainer(): HTMLDivElement;
// @internal (undocumented)
export function useLocalStore(opts?: {
persistenceKey?: string | undefined;
} & StoreOptions): StoreWithStatus;
// @internal (undocumented)
export function usePeerIds(): string[];
@ -2733,6 +2749,9 @@ export const USER_COLORS: readonly ["#FF802B", "#EC5E41", "#F2555A", "#F04F88",
// @public (undocumented)
export function useReactor(name: string, reactFn: () => void, deps?: any[] | undefined): void;
// @public (undocumented)
export function useTLStore(opts: StoreOptions): TLStore;
// @internal (undocumented)
export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10;

Wyświetl plik

@ -56,6 +56,7 @@
"crc": "^4.3.2",
"escape-string-regexp": "^5.0.0",
"eventemitter3": "^4.0.7",
"idb": "^7.1.1",
"is-plain-object": "^5.0.0",
"lodash.throttle": "^4.1.1",
"lodash.uniq": "^4.5.0",
@ -79,7 +80,7 @@
"@types/wicg-file-system-access": "^2020.9.5",
"benchmark": "^2.1.4",
"fake-indexeddb": "^4.0.0",
"jest-canvas-mock": "^2.4.0",
"jest-canvas-mock": "^2.5.1",
"jest-environment-jsdom": "^29.4.3",
"lazyrepo": "0.0.0-alpha.26",
"react-test-renderer": "^18.2.0",
@ -103,6 +104,7 @@
},
"setupFiles": [
"raf/polyfill",
"jest-canvas-mock",
"<rootDir>/setupTests.js"
],
"setupFilesAfterEnv": [

Wyświetl plik

@ -127,13 +127,14 @@ export {
export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
export {
type ErrorSyncedStore,
type InitializingSyncedStore,
type ReadySyncedStore,
type SyncedStore,
} from './lib/config/SyncedStore'
export { USER_COLORS } from './lib/config/TLUserPreferences'
export { TldrawEditorConfig } from './lib/config/TldrawEditorConfig'
USER_COLORS,
getUserPreferences,
setUserPreferences,
type TLUserPreferences,
} from './lib/config/TLUserPreferences'
export { createTLStore } from './lib/config/createTLStore'
export { defaultShapes } from './lib/config/defaultShapes'
export { defaultTools } from './lib/config/defaultTools'
export {
ANIMATION_MEDIUM_MS,
ANIMATION_SHORT_MS,
@ -176,10 +177,12 @@ export { normalizeWheel } from './lib/hooks/shared'
export { useApp } from './lib/hooks/useApp'
export { useContainer } from './lib/hooks/useContainer'
export type { TLEditorComponents } from './lib/hooks/useEditorComponents'
export { useLocalStore } from './lib/hooks/useLocalStore'
export { usePeerIds } from './lib/hooks/usePeerIds'
export { usePresence } from './lib/hooks/usePresence'
export { useQuickReactor } from './lib/hooks/useQuickReactor'
export { useReactor } from './lib/hooks/useReactor'
export { useTLStore } from './lib/hooks/useTLStore'
export { WeakMapCache } from './lib/utils/WeakMapCache'
export {
ACCEPTED_ASSET_TYPE,
@ -256,4 +259,7 @@ export {
defaultEmptyAs,
} from './lib/utils/string'
export { getPointerInfo, getSvgPathFromStroke, getSvgPathFromStrokePoints } from './lib/utils/svg'
export { type StoreWithStatus } from './lib/utils/sync/StoreWithStatus'
export { hardReset } from './lib/utils/sync/hardReset'
export { TAB_ID } from './lib/utils/sync/persistence-constants'
export { openWindow } from './lib/utils/window-open'

Wyświetl plik

@ -1,15 +1,13 @@
import { InstanceRecordType, TLAsset, TLInstanceId, TLStore } from '@tldraw/tlschema'
import { Store } from '@tldraw/tlstore'
import { TLAsset, TLInstanceId, TLRecord, TLStore } from '@tldraw/tlschema'
import { Store, StoreSnapshot } from '@tldraw/tlstore'
import { annotateError } from '@tldraw/utils'
import React, { useCallback, useMemo, useSyncExternalStore } from 'react'
import React, { memo, useCallback, useLayoutEffect, useState, useSyncExternalStore } from 'react'
import { App } from './app/App'
import { StateNodeConstructor } from './app/statechart/StateNode'
import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls'
import { OptionalErrorBoundary } from './components/ErrorBoundary'
import { SyncedStore } from './config/SyncedStore'
import { TldrawEditorConfig } from './config/TldrawEditorConfig'
import { DefaultErrorFallback } from './components/DefaultErrorFallback'
import { OptionalErrorBoundary } from './components/ErrorBoundary'
import { ShapeInfo } from './config/createTLStore'
import { AppContext } from './hooks/useApp'
import { ContainerProvider, useContainer } from './hooks/useContainer'
import { useCursor } from './hooks/useCursor'
@ -21,21 +19,38 @@ import {
} from './hooks/useEditorComponents'
import { useEvent } from './hooks/useEvent'
import { useForceUpdate } from './hooks/useForceUpdate'
import { useLocalStore } from './hooks/useLocalStore'
import { usePreloadAssets } from './hooks/usePreloadAssets'
import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix'
import { useZoomCss } from './hooks/useZoomCss'
import { StoreWithStatus } from './utils/sync/StoreWithStatus'
import { TAB_ID } from './utils/sync/persistence-constants'
/** @public */
export interface TldrawEditorProps {
export type TldrawEditorProps = {
children?: any
/** A configuration defining major customizations to the app, such as custom shapes and new tools */
config: TldrawEditorConfig
/** Overrides for the tldraw components */
components?: Partial<TLEditorComponents>
/** Whether to display the dark mode. */
isDarkMode?: boolean
/**
* Called when the app has mounted.
* An array of shape utils to use in the editor.
*/
shapes?: Record<string, ShapeInfo>
/**
* An array of tools to use in the editor.
*/
tools?: StateNodeConstructor[]
/**
* Urls for where to find fonts and other assets.
*/
assetUrls?: EditorAssetUrls
/**
* Whether to automatically focus the editor when it mounts.
*/
autoFocus?: boolean
/**
* Overrides for the tldraw user interface components.
*/
components?: Partial<TLEditorComponents>
/**
* Called when the editor has mounted.
*
* @example
*
@ -49,7 +64,7 @@ export interface TldrawEditorProps {
*/
onMount?: (app: App) => void
/**
* Called when the app generates a new asset from a file, such as when an image is dropped into
* Called when the editor generates a new asset from a file, such as when an image is dropped into
* the canvas.
*
* @example
@ -81,22 +96,31 @@ export interface TldrawEditorProps {
onCreateBookmarkFromUrl?: (
url: string
) => Promise<{ image: string; title: string; description: string }>
/**
* The Store instance to use for keeping the app's data. This may be prepopulated, e.g. by loading
* from a server or database.
*/
store?: TLStore | SyncedStore
/**
* The id of the app instance (e.g. a browser tab if the app will have only one tldraw app per
* tab). If not given, one will be generated.
*/
instanceId?: TLInstanceId
/** Asset URLs */
assetUrls?: EditorAssetUrls
/** Whether to automatically focus the editor when it mounts. */
autoFocus?: boolean
}
} & (
| {
/**
* The Store instance to use for keeping the editor's data. This may be prepopulated, e.g. by loading
* from a server or database.
*/
store: TLStore | StoreWithStatus
}
| {
store?: undefined
/**
* The editor's initial data.
*/
initialData?: StoreSnapshot<TLRecord>
/**
* The id of the editor instance (e.g. a browser tab if the editor will have only one tldraw app per
* tab). If not given, one will be generated.
*/
instanceId?: TLInstanceId
/**
* The id under which to sync and persist the editor's data.
*/
persistenceKey?: string
}
)
declare global {
interface Window {
@ -105,12 +129,15 @@ declare global {
}
/** @public */
export function TldrawEditor(props: TldrawEditorProps) {
export const TldrawEditor = memo(function TldrawEditor(props: TldrawEditorProps) {
const [container, setContainer] = React.useState<HTMLDivElement | null>(null)
const { components, ...rest } = props
const ErrorFallback =
components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback
props.components?.ErrorFallback === undefined
? DefaultErrorFallback
: props.components?.ErrorFallback
const { store, ...rest } = props
return (
<div ref={setContainer} draggable={false} className="tl-container tl-theme__light" tabIndex={0}>
@ -120,51 +147,68 @@ export function TldrawEditor(props: TldrawEditorProps) {
>
{container && (
<ContainerProvider container={container}>
<EditorComponentsProvider overrides={components}>
<TldrawEditorBeforeLoading {...rest} />
<EditorComponentsProvider overrides={props.components}>
{store ? (
store instanceof Store ? (
// Store is ready to go, whether externally synced or not
<TldrawEditorWithReadyStore {...rest} store={store} />
) : (
// Store is a synced store, so handle syncing stages internally
<TldrawEditorWithLoadingStore {...rest} store={store} />
)
) : (
// We have no store (it's undefined) so create one and possibly sync it
<TldrawEditorWithOwnStore {...rest} store={store} />
)}
</EditorComponentsProvider>
</ContainerProvider>
)}
</OptionalErrorBoundary>
</div>
)
})
function TldrawEditorWithOwnStore(props: TldrawEditorProps & { store: undefined }) {
const { initialData, instanceId = TAB_ID, shapes, persistenceKey } = props
const syncedStore = useLocalStore({
customShapes: shapes,
instanceId,
initialData,
persistenceKey,
})
return <TldrawEditorWithLoadingStore {...props} store={syncedStore} />
}
function TldrawEditorBeforeLoading({ config, instanceId, store, ...props }: TldrawEditorProps) {
const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
store,
assetUrls,
...rest
}: TldrawEditorProps & { store: StoreWithStatus }) {
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(
props.assetUrls ?? defaultEditorAssetUrls
assetUrls ?? defaultEditorAssetUrls
)
const _store = useMemo<TLStore | SyncedStore>(() => {
return (
store ??
config.createStore({
instanceId: instanceId ?? InstanceRecordType.createId(),
})
)
}, [store, config, instanceId])
let loadedStore: TLStore | SyncedStore
if (!(_store instanceof Store)) {
if (_store.error) {
switch (store.status) {
case 'error': {
// for error handling, we fall back to the default error boundary.
// if users want to handle this error differently, they can render
// their own error screen before the TldrawEditor component
throw _store.error
throw store.error
}
if (!_store.store) {
case 'loading': {
return <LoadingScreen>Connecting...</LoadingScreen>
}
loadedStore = _store.store
} else {
loadedStore = _store
}
if (instanceId && loadedStore.props.instanceId !== instanceId) {
console.error(
`The store's instanceId (${loadedStore.props.instanceId}) does not match the instanceId prop (${instanceId}). This may cause unexpected behavior.`
)
case 'not-synced': {
break
}
case 'synced-local': {
break
}
case 'synced-remote': {
break
}
}
if (preloadingError) {
@ -175,57 +219,56 @@ function TldrawEditorBeforeLoading({ config, instanceId, store, ...props }: Tldr
return <LoadingScreen>Loading assets...</LoadingScreen>
}
return <TldrawEditorAfterLoading {...props} store={loadedStore} config={config} />
}
return <TldrawEditorWithReadyStore {...rest} store={store.store} />
})
function TldrawEditorAfterLoading({
function TldrawEditorWithReadyStore({
onMount,
config,
children,
onCreateAssetFromFile,
onCreateBookmarkFromUrl,
store,
tools,
shapes,
autoFocus,
}: Omit<TldrawEditorProps, 'store' | 'config' | 'instanceId' | 'userId'> & {
config: TldrawEditorConfig
}: TldrawEditorProps & {
store: TLStore
}) {
const container = useContainer()
const [app, setApp] = React.useState<App | null>(null)
const { ErrorFallback } = useEditorComponents()
const container = useContainer()
const [app, setApp] = useState<App | null>(null)
React.useLayoutEffect(() => {
useLayoutEffect(() => {
const app = new App({
store,
config,
shapes,
tools,
getContainer: () => container,
})
setApp(app)
if (autoFocus) {
app.focus()
}
;(window as any).app = app
setApp(app)
return () => {
app.dispose()
setApp((prevApp) => (prevApp === app ? null : prevApp))
}
}, [container, config, store, autoFocus])
}, [container, shapes, tools, store])
React.useEffect(() => {
if (app) {
// Overwrite the default onCreateAssetFromFile handler.
if (onCreateAssetFromFile) {
app.onCreateAssetFromFile = onCreateAssetFromFile
}
if (!app) return
if (onCreateBookmarkFromUrl) {
app.onCreateBookmarkFromUrl = onCreateBookmarkFromUrl
}
// Overwrite the default onCreateAssetFromFile handler.
if (onCreateAssetFromFile) {
app.onCreateAssetFromFile = onCreateAssetFromFile
}
if (onCreateBookmarkFromUrl) {
app.onCreateBookmarkFromUrl = onCreateBookmarkFromUrl
}
}, [app, onCreateAssetFromFile, onCreateBookmarkFromUrl])
React.useLayoutEffect(() => {
if (app && autoFocus) app.focus()
}, [app, autoFocus])
const onMountEvent = useEvent((app: App) => {
onMount?.(app)
app.emit('mount')
@ -233,10 +276,7 @@ function TldrawEditorAfterLoading({
})
React.useEffect(() => {
if (app) {
// Run onMount
onMountEvent(app)
}
if (app) onMountEvent(app)
}, [app, onMountEvent])
const crashingError = useSyncExternalStore(

Wyświetl plik

@ -64,7 +64,7 @@ import {
isShape,
isShapeId,
} from '@tldraw/tlschema'
import { ComputedCache, HistoryEntry, UnknownRecord } from '@tldraw/tlstore'
import { ComputedCache, HistoryEntry, RecordType, UnknownRecord } from '@tldraw/tlstore'
import {
annotateError,
compact,
@ -77,7 +77,10 @@ import {
import { EventEmitter } from 'eventemitter3'
import { nanoid } from 'nanoid'
import { EMPTY_ARRAY, atom, computed, transact } from 'signia'
import { TldrawEditorConfig } from '../config/TldrawEditorConfig'
import { ShapeInfo } from '../config/createTLStore'
import { TLUser, createTLUser } from '../config/createTLUser'
import { coreShapes, defaultShapes } from '../config/defaultShapes'
import { defaultTools } from '../config/defaultTools'
import {
ANIMATION_MEDIUM_MS,
BLACKLISTED_PROPS,
@ -132,7 +135,7 @@ import { TLResizeMode, TLShapeUtil } from './shapeutils/TLShapeUtil'
import { TLTextUtil } from './shapeutils/TLTextUtil/TLTextUtil'
import { TLExportColors } from './shapeutils/shared/TLExportColors'
import { RootState } from './statechart/RootState'
import { StateNode } from './statechart/StateNode'
import { StateNode, StateNodeConstructor } from './statechart/StateNode'
import { TLClipboardModel } from './types/clipboard-types'
import { TLEventMap } from './types/emit-types'
import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo } from './types/event-types'
@ -161,8 +164,18 @@ export interface AppOptions {
* from a server or database.
*/
store: TLStore
/** A configuration defining major customizations to the app, such as custom shapes and new tools */
config: TldrawEditorConfig
/**
* An array of shapes to use in the app. These will be used to create and manage shapes in the app.
*/
shapes?: Record<string, ShapeInfo>
/**
* An array of tools to use in the app. These will be used to handle events and manage user interactions in the app.
*/
tools?: StateNodeConstructor[]
/**
* A user defined externally to replace the default user.
*/
user?: TLUser
/**
* Should return a containing html element which has all the styles applied to the app. If not
* given, the body element will be used.
@ -177,28 +190,54 @@ export function isShapeWithHandles(shape: TLShape) {
/** @public */
export class App extends EventEmitter<TLEventMap> {
constructor({ config, store, getContainer }: AppOptions) {
constructor({
store,
user,
tools = defaultTools,
shapes = defaultShapes,
getContainer,
}: AppOptions) {
super()
this.config = config
if (store.schema !== this.config.storeSchema) {
throw new Error('Store schema does not match schema given to App')
}
this.store = store
this.user = new UserPreferencesManager(this)
this.user = new UserPreferencesManager(user ?? createTLUser())
this.getContainer = getContainer ?? (() => document.body)
this.textMeasure = new TextManager(this)
// Set the shape utils
this.shapeUtils = Object.fromEntries(
Object.entries(this.config.shapeUtils).map(([type, Util]) => [type, new Util(this, type)])
this.root = new RootState(this)
// Shapes.
// Accept shapes from constructor parameters which may not conflict with the root note's core tools.
const shapeUtils = Object.fromEntries(
Object.values(coreShapes).map(({ util: Util }) => [Util.type, new Util(this, Util.type)])
)
for (const [type, { util: Util }] of Object.entries(shapes)) {
if (shapeUtils[type]) {
throw Error(`May not overwrite core shape of type "${type}".`)
}
if (type !== Util.type) {
throw Error(`Shape util's type "${Util.type}" does not match provided type "${type}".`)
}
shapeUtils[type] = new Util(this, Util.type)
}
this.shapeUtils = shapeUtils
// Tools.
// Accept tools from constructor parameters which may not conflict with the root note's default or
// "baked in" tools, select and zoom.
const uniqueTools = Object.fromEntries(tools.map((Ctor) => [Ctor.id, Ctor]))
for (const [id, Ctor] of Object.entries(uniqueTools)) {
if (this.root.children?.[id]) {
throw Error(`Can't override tool with id "${id}"`)
}
this.root.children![id] = new Ctor(this)
}
if (typeof window !== 'undefined' && 'navigator' in window) {
this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
this.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i)
@ -212,13 +251,6 @@ export class App extends EventEmitter<TLEventMap> {
// Set styles
this.colors = new Map(App.styles.color.map((c) => [c.id, `var(--palette-${c.id})`]))
this.root = new RootState(this)
if (this.root.children) {
this.config.tools.forEach((Ctor) => {
this.root.children![Ctor.id] = new Ctor(this)
})
}
this.store.onBeforeDelete = (record) => {
if (record.typeName === 'shape') {
this._shapeWillBeDeleted(record)
@ -310,13 +342,6 @@ export class App extends EventEmitter<TLEventMap> {
*/
readonly store: TLStore
/**
* The editor's config
*
* @public
*/
readonly config: TldrawEditorConfig
/**
* The root state of the statechart.
*
@ -4699,7 +4724,12 @@ export class App extends EventEmitter<TLEventMap> {
// When we create the shape, take in the partial (the props coming into the
// function) and merge it with the default props.
let shapeRecordToCreate = this.config.TLShape.create({
let shapeRecordToCreate = (
this.store.schema.types.shape as RecordType<
TLShape,
'type' | 'props' | 'index' | 'parentId'
>
).create({
...partial,
index,
parentId: partial.parentId ?? focusLayerId,

Wyświetl plik

@ -1,37 +1,37 @@
import { TLUserPreferences } from '../../config/TLUserPreferences'
import { App } from '../App'
import { TLUser } from '../../config/createTLUser'
export class UserPreferencesManager {
constructor(private readonly editor: App) {}
constructor(private readonly user: TLUser) {}
updateUserPreferences = (userPreferences: Partial<TLUserPreferences>) => {
this.editor.config.setUserPreferences({
...this.editor.config.userPreferences.value,
this.user.setUserPreferences({
...this.user.userPreferences.value,
...userPreferences,
})
}
get isDarkMode() {
return this.editor.config.userPreferences.value.isDarkMode
return this.user.userPreferences.value.isDarkMode
}
get animationSpeed() {
return this.editor.config.userPreferences.value.animationSpeed
return this.user.userPreferences.value.animationSpeed
}
get id() {
return this.editor.config.userPreferences.value.id
return this.user.userPreferences.value.id
}
get name() {
return this.editor.config.userPreferences.value.name
return this.user.userPreferences.value.name
}
get locale() {
return this.editor.config.userPreferences.value.locale
return this.user.userPreferences.value.locale
}
get color() {
return this.editor.config.userPreferences.value.color
return this.user.userPreferences.value.color
}
}

Wyświetl plik

@ -8,6 +8,8 @@ import { DashedOutlineBox } from '../shared/DashedOutlineBox'
export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
static override type = 'group'
type = 'group' as const
hideSelectionBoundsBg = () => false
hideSelectionBoundsFg = () => true

Wyświetl plik

@ -24,6 +24,7 @@ export interface TLShapeUtilConstructor<
ShapeUtil extends TLShapeUtil<T> = TLShapeUtil<T>
> {
new (app: App, type: T['type']): ShapeUtil
type: T['type']
}
/** @public */

Wyświetl plik

@ -1,37 +1,12 @@
import { TLEventHandlers } from '../types/event-types'
import { StateNode } from './StateNode'
import { TLArrowTool } from './TLArrowTool/TLArrowTool'
import { TLDrawTool } from './TLDrawTool/TLDrawTool'
import { TLEraserTool } from './TLEraserTool/TLEraserTool'
import { TLFrameTool } from './TLFrameTool/TLFrameTool'
import { TLGeoTool } from './TLGeoTool/TLGeoTool'
import { TLHandTool } from './TLHandTool/TLHandTool'
import { TLHighlightTool } from './TLHighlightTool/TLHighlightTool'
import { TLLaserTool } from './TLLaserTool/TLLaserTool'
import { TLLineTool } from './TLLineTool/TLLineTool'
import { TLNoteTool } from './TLNoteTool/TLNoteTool'
import { TLSelectTool } from './TLSelectTool/TLSelectTool'
import { TLTextTool } from './TLTextTool/TLTextTool'
import { TLZoomTool } from './TLZoomTool/TLZoomTool'
export class RootState extends StateNode {
static override id = 'root'
static initial = 'select'
static children = () => [
TLSelectTool,
TLHandTool,
TLEraserTool,
TLDrawTool,
TLHighlightTool,
TLTextTool,
TLLineTool,
TLArrowTool,
TLGeoTool,
TLNoteTool,
TLFrameTool,
TLZoomTool,
TLLaserTool,
]
static children = () => [TLSelectTool, TLZoomTool]
onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
switch (info.code) {

Wyświetl plik

@ -5,6 +5,7 @@ import { Lasering } from './children/Lasering'
export class TLLaserTool extends StateNode {
static override id = 'laser'
static initial = 'idle'
static children = () => [Idle, Lasering]

Wyświetl plik

@ -1,25 +0,0 @@
import { TLStore } from '@tldraw/tlschema'
/** @public */
export interface ReadySyncedStore {
readonly status: 'synced'
readonly store: TLStore
readonly error?: undefined
}
/** @public */
export interface ErrorSyncedStore {
readonly status: 'error'
readonly store?: undefined
readonly error: Error
}
/** @public */
export interface InitializingSyncedStore {
readonly status: 'loading'
readonly store?: undefined
readonly error?: undefined
}
/** @public */
export type SyncedStore = ReadySyncedStore | ErrorSyncedStore | InitializingSyncedStore

Wyświetl plik

@ -146,6 +146,7 @@ function storeUserPreferences() {
}
}
/** @public */
export function setUserPreferences(user: TLUserPreferences) {
userTypeValidator.validate(user)
globalUserPreferences.set(user)

Wyświetl plik

@ -1,131 +0,0 @@
import {
CLIENT_FIXUP_SCRIPT,
InstanceRecordType,
TLDOCUMENT_ID,
TLDefaultShape,
TLInstanceId,
TLInstancePresence,
TLRecord,
TLShape,
TLStore,
TLStoreProps,
createTLSchema,
} from '@tldraw/tlschema'
import { Migrations, RecordType, Store, StoreSchema, StoreSnapshot } from '@tldraw/tlstore'
import { Signal, computed } from 'signia'
import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil'
import { TLBookmarkUtil } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
import { TLDrawUtil } from '../app/shapeutils/TLDrawUtil/TLDrawUtil'
import { TLEmbedUtil } from '../app/shapeutils/TLEmbedUtil/TLEmbedUtil'
import { TLFrameUtil } from '../app/shapeutils/TLFrameUtil/TLFrameUtil'
import { TLGeoUtil } from '../app/shapeutils/TLGeoUtil/TLGeoUtil'
import { TLGroupUtil } from '../app/shapeutils/TLGroupUtil/TLGroupUtil'
import { TLHighlightUtil } from '../app/shapeutils/TLHighlightUtil/TLHighlightUtil'
import { TLImageUtil } from '../app/shapeutils/TLImageUtil/TLImageUtil'
import { TLLineUtil } from '../app/shapeutils/TLLineUtil/TLLineUtil'
import { TLNoteUtil } from '../app/shapeutils/TLNoteUtil/TLNoteUtil'
import { TLShapeUtilConstructor } from '../app/shapeutils/TLShapeUtil'
import { TLTextUtil } from '../app/shapeutils/TLTextUtil/TLTextUtil'
import { TLVideoUtil } from '../app/shapeutils/TLVideoUtil/TLVideoUtil'
import { StateNodeConstructor } from '../app/statechart/StateNode'
import { TLUserPreferences, getUserPreferences, setUserPreferences } from './TLUserPreferences'
// Secret shape types that don't have a shape util yet
type ShapeTypesNotImplemented = 'icon'
const DEFAULT_SHAPE_UTILS: {
[K in Exclude<TLDefaultShape['type'], ShapeTypesNotImplemented>]: TLShapeUtilConstructor<any>
} = {
arrow: TLArrowUtil,
bookmark: TLBookmarkUtil,
draw: TLDrawUtil,
embed: TLEmbedUtil,
frame: TLFrameUtil,
geo: TLGeoUtil,
group: TLGroupUtil,
image: TLImageUtil,
line: TLLineUtil,
note: TLNoteUtil,
text: TLTextUtil,
video: TLVideoUtil,
highlight: TLHighlightUtil,
}
/** @public */
export type TldrawEditorConfigOptions = {
tools?: readonly StateNodeConstructor[]
shapes?: Record<
string,
{
util: TLShapeUtilConstructor<any>
validator?: { validate: <T>(record: T) => T }
migrations?: Migrations
}
>
/** @internal */
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
userPreferences?: Signal<TLUserPreferences>
setUserPreferences?: (userPreferences: TLUserPreferences) => void
}
/** @public */
export class TldrawEditorConfig {
// Custom tools
readonly tools: readonly StateNodeConstructor[]
// Custom shape utils
readonly shapeUtils: Record<TLShape['type'], TLShapeUtilConstructor<any>>
// The record used for TLShape incorporating any custom shapes
readonly TLShape: RecordType<TLShape, 'type' | 'props' | 'index' | 'parentId'>
// The schema used for the store incorporating any custom shapes
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>
readonly derivePresenceState: (store: TLStore) => Signal<TLInstancePresence | null>
readonly userPreferences: Signal<TLUserPreferences>
readonly setUserPreferences: (userPreferences: TLUserPreferences) => void
constructor(opts = {} as TldrawEditorConfigOptions) {
const { shapes = {}, tools = [], derivePresenceState } = opts
this.tools = tools
this.derivePresenceState = derivePresenceState ?? (() => computed('presence', () => null))
this.userPreferences =
opts.userPreferences ?? computed('userPreferences', () => getUserPreferences())
this.setUserPreferences = opts.setUserPreferences ?? setUserPreferences
this.shapeUtils = {
...DEFAULT_SHAPE_UTILS,
...Object.fromEntries(Object.entries(shapes).map(([k, v]) => [k, v.util])),
}
this.storeSchema = createTLSchema({
customShapes: shapes,
})
this.TLShape = this.storeSchema.types.shape as RecordType<
TLShape,
'type' | 'props' | 'index' | 'parentId'
>
}
createStore(config: {
/** The store's initial data. */
initialData?: StoreSnapshot<TLRecord>
instanceId: TLInstanceId
}): TLStore {
let initialData = config.initialData
if (initialData) {
initialData = CLIENT_FIXUP_SCRIPT(initialData)
}
return new Store<TLRecord, TLStoreProps>({
schema: this.storeSchema,
initialData,
props: {
instanceId: config?.instanceId ?? InstanceRecordType.createId(),
documentId: TLDOCUMENT_ID,
},
})
}
}

Wyświetl plik

@ -0,0 +1,43 @@
import {
InstanceRecordType,
TLDOCUMENT_ID,
TLInstanceId,
TLRecord,
TLStore,
createTLSchema,
} from '@tldraw/tlschema'
import { Migrations, Store, StoreSnapshot } from '@tldraw/tlstore'
import { TLShapeUtilConstructor } from '../app/shapeutils/TLShapeUtil'
/** @public */
export type ShapeInfo = {
util: TLShapeUtilConstructor<any>
migrations?: Migrations
validator?: { validate: (record: any) => any }
}
/** @public */
export type StoreOptions = {
customShapes?: Record<string, ShapeInfo>
instanceId?: TLInstanceId
initialData?: StoreSnapshot<TLRecord>
}
/**
* A helper for creating a TLStore. Custom shapes cannot override default shapes.
*
* @param opts - Options for creating the store.
*
* @public */
export function createTLStore(opts = {} as StoreOptions): TLStore {
const { customShapes = {}, instanceId = InstanceRecordType.createId(), initialData } = opts
return new Store({
schema: createTLSchema({ customShapes }),
initialData,
props: {
instanceId,
documentId: TLDOCUMENT_ID,
},
})
}

Wyświetl plik

@ -0,0 +1,27 @@
import { TLInstancePresence, TLStore } from '@tldraw/tlschema'
import { Signal, computed } from 'signia'
import { TLUserPreferences, getUserPreferences, setUserPreferences } from './TLUserPreferences'
/** @public */
export interface TLUser {
readonly derivePresenceState: (store: TLStore) => Signal<TLInstancePresence | null>
readonly userPreferences: Signal<TLUserPreferences>
readonly setUserPreferences: (userPreferences: TLUserPreferences) => void
}
/** @public */
export function createTLUser(
opts = {} as {
/** @internal */
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
userPreferences?: Signal<TLUserPreferences>
setUserPreferences?: (userPreferences: TLUserPreferences) => void
}
): TLUser {
return {
derivePresenceState: opts.derivePresenceState ?? (() => computed('presence', () => null)),
userPreferences:
opts.userPreferences ?? computed('userPreferences', () => getUserPreferences()),
setUserPreferences: opts.setUserPreferences ?? setUserPreferences,
}
}

Wyświetl plik

@ -0,0 +1,121 @@
import {
arrowShapeTypeMigrations,
arrowShapeTypeValidator,
bookmarkShapeTypeMigrations,
bookmarkShapeTypeValidator,
drawShapeTypeMigrations,
drawShapeTypeValidator,
embedShapeTypeMigrations,
embedShapeTypeValidator,
frameShapeTypeMigrations,
frameShapeTypeValidator,
geoShapeTypeMigrations,
geoShapeTypeValidator,
groupShapeTypeMigrations,
groupShapeTypeValidator,
highlightShapeTypeMigrations,
highlightShapeTypeValidator,
imageShapeTypeMigrations,
imageShapeTypeValidator,
lineShapeTypeMigrations,
lineShapeTypeValidator,
noteShapeTypeMigrations,
noteShapeTypeValidator,
textShapeTypeMigrations,
textShapeTypeValidator,
videoShapeTypeMigrations,
videoShapeTypeValidator,
} from '@tldraw/tlschema'
import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil'
import { TLBookmarkUtil } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
import { TLDrawUtil } from '../app/shapeutils/TLDrawUtil/TLDrawUtil'
import { TLEmbedUtil } from '../app/shapeutils/TLEmbedUtil/TLEmbedUtil'
import { TLFrameUtil } from '../app/shapeutils/TLFrameUtil/TLFrameUtil'
import { TLGeoUtil } from '../app/shapeutils/TLGeoUtil/TLGeoUtil'
import { TLGroupUtil } from '../app/shapeutils/TLGroupUtil/TLGroupUtil'
import { TLHighlightUtil } from '../app/shapeutils/TLHighlightUtil/TLHighlightUtil'
import { TLImageUtil } from '../app/shapeutils/TLImageUtil/TLImageUtil'
import { TLLineUtil } from '../app/shapeutils/TLLineUtil/TLLineUtil'
import { TLNoteUtil } from '../app/shapeutils/TLNoteUtil/TLNoteUtil'
import { TLTextUtil } from '../app/shapeutils/TLTextUtil/TLTextUtil'
import { TLVideoUtil } from '../app/shapeutils/TLVideoUtil/TLVideoUtil'
import { ShapeInfo } from './createTLStore'
/** @public */
export const coreShapes: Record<string, ShapeInfo> = {
// created by grouping interactions, probably the corest core shape that we have
group: {
util: TLGroupUtil,
validator: groupShapeTypeValidator,
migrations: groupShapeTypeMigrations,
},
// created by embed menu / url drop
embed: {
util: TLEmbedUtil,
validator: embedShapeTypeValidator,
migrations: embedShapeTypeMigrations,
},
// created by copy and paste / url drop
bookmark: {
util: TLBookmarkUtil,
validator: bookmarkShapeTypeValidator,
migrations: bookmarkShapeTypeMigrations,
},
// created by copy and paste / file drop
image: {
util: TLImageUtil,
validator: imageShapeTypeValidator,
migrations: imageShapeTypeMigrations,
},
// created by copy and paste / file drop
video: {
util: TLVideoUtil,
validator: videoShapeTypeValidator,
migrations: videoShapeTypeMigrations,
},
// created by copy and paste
text: {
util: TLTextUtil,
validator: textShapeTypeValidator,
migrations: textShapeTypeMigrations,
},
}
/** @public */
export const defaultShapes: Record<string, ShapeInfo> = {
draw: {
util: TLDrawUtil,
validator: drawShapeTypeValidator,
migrations: drawShapeTypeMigrations,
},
geo: {
util: TLGeoUtil,
validator: geoShapeTypeValidator,
migrations: geoShapeTypeMigrations,
},
line: {
util: TLLineUtil,
validator: lineShapeTypeValidator,
migrations: lineShapeTypeMigrations,
},
note: {
util: TLNoteUtil,
validator: noteShapeTypeValidator,
migrations: noteShapeTypeMigrations,
},
frame: {
util: TLFrameUtil,
validator: frameShapeTypeValidator,
migrations: frameShapeTypeMigrations,
},
arrow: {
util: TLArrowUtil,
validator: arrowShapeTypeValidator,
migrations: arrowShapeTypeMigrations,
},
highlight: {
util: TLHighlightUtil,
validator: highlightShapeTypeValidator,
migrations: highlightShapeTypeMigrations,
},
}

Wyświetl plik

@ -0,0 +1,27 @@
import { StateNodeConstructor } from '../app/statechart/StateNode'
import { TLArrowTool } from '../app/statechart/TLArrowTool/TLArrowTool'
import { TLDrawTool } from '../app/statechart/TLDrawTool/TLDrawTool'
import { TLEraserTool } from '../app/statechart/TLEraserTool/TLEraserTool'
import { TLFrameTool } from '../app/statechart/TLFrameTool/TLFrameTool'
import { TLGeoTool } from '../app/statechart/TLGeoTool/TLGeoTool'
import { TLHandTool } from '../app/statechart/TLHandTool/TLHandTool'
import { TLHighlightTool } from '../app/statechart/TLHighlightTool/TLHighlightTool'
import { TLLaserTool } from '../app/statechart/TLLaserTool/TLLaserTool'
import { TLLineTool } from '../app/statechart/TLLineTool/TLLineTool'
import { TLNoteTool } from '../app/statechart/TLNoteTool/TLNoteTool'
import { TLTextTool } from '../app/statechart/TLTextTool/TLTextTool'
/** @public */
export const defaultTools: StateNodeConstructor[] = [
TLHandTool,
TLEraserTool,
TLLaserTool,
TLDrawTool,
TLTextTool,
TLLineTool,
TLArrowTool,
TLGeoTool,
TLNoteTool,
TLFrameTool,
TLHighlightTool,
]

Wyświetl plik

@ -4,12 +4,14 @@ import { useApp } from './useApp'
export function useCoarsePointer() {
const app = useApp()
useEffect(() => {
const mql = window.matchMedia('(pointer: coarse)')
const handler = () => {
app.isCoarsePointer = mql.matches
if (window.matchMedia) {
const mql = window.matchMedia('(pointer: coarse)')
const handler = () => {
app.isCoarsePointer = mql.matches
}
handler()
mql.addEventListener('change', handler)
return () => mql.removeEventListener('change', handler)
}
handler()
mql.addEventListener('change', handler)
return () => mql.removeEventListener('change', handler)
}, [app])
}

Wyświetl plik

@ -0,0 +1,59 @@
import { useEffect, useState } from 'react'
import { StoreOptions } from '../config/createTLStore'
import { uniqueId } from '../utils/data'
import { StoreWithStatus } from '../utils/sync/StoreWithStatus'
import { TLLocalSyncClient } from '../utils/sync/TLLocalSyncClient'
import { useTLStore } from './useTLStore'
/** @internal */
export function useLocalStore(
opts = {} as { persistenceKey?: string } & StoreOptions
): StoreWithStatus {
const { persistenceKey, ...rest } = opts
const [state, setState] = useState<{ id: string; storeWithStatus: StoreWithStatus } | null>(null)
const store = useTLStore(rest)
useEffect(() => {
const id = uniqueId()
if (!persistenceKey) {
setState({
id,
storeWithStatus: { status: 'not-synced', store },
})
return
}
setState({
id,
storeWithStatus: { status: 'loading' },
})
const setStoreWithStatus = (storeWithStatus: StoreWithStatus) => {
setState((prev) => {
if (prev?.id === id) {
return { id, storeWithStatus }
}
return prev
})
}
const client = new TLLocalSyncClient(store, {
universalPersistenceKey: persistenceKey,
onLoad() {
setStoreWithStatus({ store, status: 'synced-local' })
},
onLoadError(err: any) {
setStoreWithStatus({ status: 'error', error: err })
},
})
return () => {
setState((prevState) => (prevState?.id === id ? null : prevState))
client.close()
}
}, [persistenceKey, store])
return state?.storeWithStatus ?? { status: 'loading' }
}

Wyświetl plik

@ -14,7 +14,7 @@ const generateImage = (dpr: number, currentZoom: number, darkMode: boolean) => {
canvasEl.height = size
const ctx = canvasEl.getContext('2d')
if (!ctx) throw new Error('No canvas')
if (!ctx) return
ctx.fillStyle = darkMode ? '#212529' : '#f8f9fa'
ctx.fillRect(0, 0, size, size)
@ -53,7 +53,9 @@ const canvasBlob = (size: [number, number], fn: (ctx: CanvasRenderingContext2D)
const canvas = document.createElement('canvas')
canvas.width = size[0]
canvas.height = size[1]
fn(canvas.getContext('2d')!)
const ctx = canvas.getContext('2d')
if (!ctx) return ''
fn(ctx)
return canvas.toDataURL()
}
type PatternDef = { zoom: number; url: string; darkMode: boolean }

Wyświetl plik

@ -0,0 +1,10 @@
import { useEffect, useRef } from 'react'
/** @internal */
export function usePrevious<T>(value: T) {
const ref = useRef(value)
useEffect(() => {
ref.current = value
})
return ref.current
}

Wyświetl plik

@ -0,0 +1,19 @@
import { useState } from 'react'
import { StoreOptions, createTLStore } from '../config/createTLStore'
import { usePrevious } from './usePrevious'
/** @public */
export function useTLStore(opts: StoreOptions) {
const [store, setStore] = useState(() => createTLStore(opts))
const previousOpts = usePrevious(opts)
if (
previousOpts.customShapes !== opts.customShapes ||
previousOpts.initialData !== opts.initialData ||
previousOpts.instanceId !== opts.instanceId
) {
const newStore = createTLStore(opts)
setStore(newStore)
return newStore
}
return store
}

Wyświetl plik

@ -26,7 +26,9 @@ import {
TLWheelEventInfo,
} from '../app/types/event-types'
import { RequiredKeys } from '../app/types/misc-types'
import { TldrawEditorConfig } from '../config/TldrawEditorConfig'
import { createTLStore } from '../config/createTLStore'
import { defaultShapes } from '../config/defaultShapes'
import { defaultTools } from '../config/defaultTools'
import { shapesFromJsx } from './jsx'
jest.useFakeTimers()
@ -56,12 +58,14 @@ export const TEST_INSTANCE_ID = InstanceRecordType.createCustomId('testInstance1
export class TestApp extends App {
constructor(options = {} as Partial<Omit<AppOptions, 'store'>>) {
const elm = document.createElement('div')
const { shapes = {}, tools = [] } = options
elm.tabIndex = 0
const config = options.config ?? new TldrawEditorConfig()
super({
config,
store: config.createStore({
shapes: { ...defaultShapes, ...shapes },
tools: [...defaultTools, ...tools],
store: createTLStore({
instanceId: TEST_INSTANCE_ID,
customShapes: shapes,
}),
getContainer: () => elm,
...options,

Wyświetl plik

@ -1,7 +1,12 @@
import { render, screen } from '@testing-library/react'
import { InstanceRecordType } from '@tldraw/tlschema'
import { act, render, screen } from '@testing-library/react'
import { InstanceRecordType, TLBaseShape, TLOpacityType } from '@tldraw/tlschema'
import { TldrawEditor } from '../TldrawEditor'
import { TldrawEditorConfig } from '../config/TldrawEditorConfig'
import { App } from '../app/App'
import { TLBoxUtil } from '../app/shapeutils/TLBoxUtil'
import { TLBoxTool } from '../app/statechart/TLBoxTool/TLBoxTool'
import { Canvas } from '../components/Canvas'
import { HTMLContainer } from '../components/HTMLContainer'
import { createTLStore } from '../config/createTLStore'
let originalFetch: typeof window.fetch
beforeEach(() => {
@ -9,7 +14,6 @@ beforeEach(() => {
if (args[0] === '/icons/icon/icon-names.json') {
return Promise.resolve({ json: () => Promise.resolve([]) } as Response)
}
return originalFetch(...args)
})
})
@ -19,43 +23,75 @@ afterEach(() => {
window.fetch = originalFetch
})
describe('<Tldraw />', () => {
it('Accepts fresh versions of store and calls `onMount` for each one', async () => {
const config = new TldrawEditorConfig()
describe('<TldrawEditor />', () => {
it('Renders without crashing', async () => {
await act(async () => (
<TldrawEditor autoFocus>
<div data-testid="canvas-1" />
</TldrawEditor>
))
})
const initialStore = config.createStore({
it('Creates its own store', async () => {
let store: any
render(
await act(async () => (
<TldrawEditor onMount={(app) => (store = app.store)} autoFocus>
<div data-testid="canvas-1" />
</TldrawEditor>
))
)
await screen.findByTestId('canvas-1')
expect(store).toBeTruthy()
})
it('Renders with an external store', async () => {
const store = createTLStore()
render(
await act(async () => (
<TldrawEditor
store={store}
onMount={(app) => {
expect(app.store).toBe(store)
}}
autoFocus
>
<div data-testid="canvas-1" />
</TldrawEditor>
))
)
await screen.findByTestId('canvas-1')
})
it('Accepts fresh versions of store and calls `onMount` for each one', async () => {
const initialStore = createTLStore({
instanceId: InstanceRecordType.createCustomId('test'),
})
const onMount = jest.fn()
const rendered = render(
<TldrawEditor config={config} store={initialStore} onMount={onMount} autoFocus>
<TldrawEditor store={initialStore} onMount={onMount} autoFocus>
<div data-testid="canvas-1" />
</TldrawEditor>
)
await screen.findByTestId('canvas-1')
expect(onMount).toHaveBeenCalledTimes(1)
const initialApp = onMount.mock.lastCall[0]
jest.spyOn(initialApp, 'dispose')
expect(initialApp.store).toBe(initialStore)
// re-render with the same store:
rendered.rerender(
<TldrawEditor config={config} store={initialStore} onMount={onMount} autoFocus>
<TldrawEditor store={initialStore} onMount={onMount} autoFocus>
<div data-testid="canvas-2" />
</TldrawEditor>
)
await screen.findByTestId('canvas-2')
// not called again:
expect(onMount).toHaveBeenCalledTimes(1)
// re-render with a new store:
const newStore = config.createStore({
const newStore = createTLStore({
instanceId: InstanceRecordType.createCustomId('test'),
})
rendered.rerender(
<TldrawEditor config={config} store={newStore} onMount={onMount} autoFocus>
<TldrawEditor store={newStore} onMount={onMount} autoFocus>
<div data-testid="canvas-3" />
</TldrawEditor>
)
@ -64,4 +100,188 @@ describe('<Tldraw />', () => {
expect(onMount).toHaveBeenCalledTimes(2)
expect(onMount.mock.lastCall[0].store).toBe(newStore)
})
it('Renders the canvas and shapes', async () => {
let app = {} as App
render(
await act(async () => (
<TldrawEditor
autoFocus
onMount={(editorApp) => {
app = editorApp
}}
>
<Canvas />
<div data-testid="canvas-1" />
</TldrawEditor>
))
)
await screen.findByTestId('canvas-1')
expect(app).toBeTruthy()
await act(async () => {
app.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } }, true, true)
})
const id = app.createShapeId()
await act(async () => {
app.createShapes([
{
id,
type: 'geo',
props: { w: 100, h: 100 },
},
])
})
// Does the shape exist?
expect(app.getShapeById(id)).toMatchObject({
id,
type: 'geo',
x: 0,
y: 0,
props: { geo: 'rectangle', w: 100, h: 100, opacity: '1' },
})
// Is the shape's component rendering?
expect(document.querySelectorAll('.tl-shape')).toHaveLength(1)
expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(0)
// Select the shape
await act(async () => app.select(id))
// Is the shape's component rendering?
expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
// Select the eraser tool...
await act(async () => app.setSelectedTool('eraser'))
// Is the editor's current tool correct?
expect(app.currentToolId).toBe('eraser')
})
})
describe('Custom shapes', () => {
type CardShape = TLBaseShape<
'card',
{
w: number
h: number
opacity: TLOpacityType
}
>
class CardUtil extends TLBoxUtil<CardShape> {
static override type = 'card' as const
override isAspectRatioLocked = (_shape: CardShape) => false
override canResize = (_shape: CardShape) => true
override canBind = (_shape: CardShape) => true
override defaultProps(): CardShape['props'] {
return {
opacity: '1',
w: 300,
h: 300,
}
}
render(shape: CardShape) {
const bounds = this.bounds(shape)
return (
<HTMLContainer
id={shape.id}
data-testid="card-shape"
style={{
border: '1px solid black',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'all',
}}
>
{bounds.w.toFixed()}x{bounds.h.toFixed()}
</HTMLContainer>
)
}
indicator(shape: CardShape) {
return <rect data-testid="card-indicator" width={shape.props.w} height={shape.props.h} />
}
}
class CardTool extends TLBoxTool {
static override id = 'card'
static override initial = 'idle'
override shapeType = 'card'
}
const tools = [CardTool]
const shapes = { card: { util: CardUtil } }
it('Uses custom shapes', async () => {
let app = {} as App
render(
await act(async () => (
<TldrawEditor
shapes={shapes}
tools={tools}
autoFocus
onMount={(editorApp) => {
app = editorApp
}}
>
<Canvas />
<div data-testid="canvas-1" />
</TldrawEditor>
))
)
await screen.findByTestId('canvas-1')
expect(app).toBeTruthy()
await act(async () => {
app.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } }, true, true)
})
expect(app.shapeUtils.card).toBeTruthy()
const id = app.createShapeId()
await act(async () => {
app.createShapes([
{
id,
type: 'card',
props: { w: 100, h: 100 },
},
])
})
// Does the shape exist?
expect(app.getShapeById(id)).toMatchObject({
id,
type: 'card',
x: 0,
y: 0,
props: { w: 100, h: 100, opacity: '1' },
})
// Is the shape's component rendering?
expect(await screen.findByTestId('card-shape')).toBeTruthy()
// Select the shape
await act(async () => app.select(id))
// Is the shape's component rendering?
expect(await screen.findByTestId('card-indicator')).toBeTruthy()
// Select the tool...
await act(async () => app.setSelectedTool('card'))
// Is the editor's current tool correct?
expect(app.currentToolId).toBe('card')
})
})

Wyświetl plik

@ -2,9 +2,9 @@ import { Box2d, Vec2d, VecLike } from '@tldraw/primitives'
import { TLShapeId, TLShapePartial, Vec2dModel, createCustomShapeId } from '@tldraw/tlschema'
import { GapsSnapLine, PointsSnapLine, SnapLine } from '../../app/managers/SnapManager'
import { TLShapeUtil } from '../../app/shapeutils/TLShapeUtil'
import { TldrawEditorConfig } from '../../config/TldrawEditorConfig'
import { TestApp } from '../TestApp'
import { defaultShapes } from '../../config/defaultShapes'
import { getSnapLines } from '../testutils/getSnapLines'
type __TopLeftSnapOnlyShape = any
@ -40,14 +40,6 @@ class __TopLeftSnapOnlyShapeUtil extends TLShapeUtil<__TopLeftSnapOnlyShape> {
}
}
const configWithCustomShape = new TldrawEditorConfig({
shapes: {
__test_top_left_snap_only: {
util: __TopLeftSnapOnlyShapeUtil,
},
},
})
let app: TestApp
afterEach(() => {
@ -759,8 +751,12 @@ describe('custom snapping points', () => {
beforeEach(() => {
app?.dispose()
app = new TestApp({
config: configWithCustomShape,
shapes: {
...defaultShapes,
__test_top_left_snap_only: {
util: __TopLeftSnapOnlyShapeUtil,
},
},
// x───────┐
// │ T │
// │ │

Wyświetl plik

@ -0,0 +1,30 @@
import { TLStore } from '@tldraw/tlschema'
/** @public */
export type StoreWithStatus =
| {
readonly status: 'not-synced'
readonly store: TLStore
readonly error?: undefined
}
| {
readonly status: 'error'
readonly store?: undefined
readonly error: Error
}
| {
readonly status: 'loading'
readonly store?: undefined
readonly error?: undefined
}
| {
readonly status: 'synced-local'
readonly store: TLStore
readonly error?: undefined
}
| {
readonly status: 'synced-remote'
readonly connectionStatus: 'online' | 'offline'
readonly store: TLStore
readonly error?: undefined
}

Wyświetl plik

@ -1,12 +1,8 @@
import {
InstanceRecordType,
PageRecordType,
TldrawEditorConfig,
TLInstanceId,
} from '@tldraw/editor'
import { InstanceRecordType, PageRecordType, TLInstanceId } from '@tldraw/tlschema'
import { promiseWithResolve } from '@tldraw/utils'
import * as idb from './indexedDb'
import { createTLStore } from '../../config/createTLStore'
import { TLLocalSyncClient } from './TLLocalSyncClient'
import * as idb from './indexedDb'
jest.mock('./indexedDb', () => ({
...jest.requireActual('./indexedDb'),
@ -31,7 +27,7 @@ function testClient(
instanceId: TLInstanceId = InstanceRecordType.createCustomId('test'),
channel = new BroadcastChannelMock('test')
) {
const store = new TldrawEditorConfig().createStore({
const store = createTLStore({
instanceId,
})
const onLoad = jest.fn(() => {

Wyświetl plik

@ -1,4 +1,4 @@
import { TLInstanceId, TLRecord, TLStore } from '@tldraw/editor'
import { TLInstanceId, TLRecord, TLStore } from '@tldraw/tlschema'
import { RecordsDiff, SerializedSchema, compareSchemas, squashRecordDiffs } from '@tldraw/tlstore'
import { assert, hasOwnProperty } from '@tldraw/utils'
import { transact } from 'signia'

Wyświetl plik

@ -1,4 +1,4 @@
import { TLRecord, TLStoreSchema } from '@tldraw/editor'
import { TLRecord, TLStoreSchema } from '@tldraw/tlschema'
import { RecordsDiff, SerializedSchema, StoreSnapshot } from '@tldraw/tlstore'
import { IDBPDatabase, openDB } from 'idb'
import { STORE_PREFIX, addDbName, getAllIndexDbNames } from './persistence-constants'

Wyświetl plik

@ -1,4 +1,5 @@
import { InstanceRecordType, TLInstanceId, uniqueId } from '@tldraw/editor'
import { InstanceRecordType, TLInstanceId } from '@tldraw/tlschema'
import { uniqueId } from '../data'
const tabIdKey = 'TLDRAW_TAB_ID_v2' as const

Wyświetl plik

@ -8,7 +8,6 @@ import { App } from '@tldraw/editor';
import { MigrationFailureReason } from '@tldraw/tlstore';
import { Result } from '@tldraw/utils';
import { SerializedSchema } from '@tldraw/tlstore';
import { TldrawEditorConfig } from '@tldraw/editor';
import { TLInstanceId } from '@tldraw/editor';
import { TLStore } from '@tldraw/editor';
import { TLTranslationKey } from '@tldraw/ui';
@ -22,8 +21,8 @@ export function isV1File(data: any): boolean;
export function parseAndLoadDocument(app: App, document: string, msg: (id: TLTranslationKey) => string, addToast: ToastsContextType['addToast'], onV1FileLoad?: () => void, forceDarkMode?: boolean): Promise<void>;
// @public (undocumented)
export function parseTldrawJsonFile({ config, json, instanceId, }: {
config: TldrawEditorConfig;
export function parseTldrawJsonFile({ json, instanceId, store, }: {
store: TLStore;
json: string;
instanceId: TLInstanceId;
}): Result<TLStore, TldrawFileParseError>;

Wyświetl plik

@ -1,9 +1,9 @@
import {
App,
buildFromV1Document,
createTLStore,
fileToBase64,
TLAsset,
TldrawEditorConfig,
TLInstanceId,
TLRecord,
TLStore,
@ -81,11 +81,11 @@ export type TldrawFileParseError =
/** @public */
export function parseTldrawJsonFile({
config,
json,
instanceId,
store,
}: {
config: TldrawEditorConfig
store: TLStore
json: string
instanceId: TLInstanceId
}): Result<TLStore, TldrawFileParseError> {
@ -123,7 +123,7 @@ export function parseTldrawJsonFile({
let migrationResult: MigrationResult<StoreSnapshot<TLRecord>>
try {
const storeSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r as TLRecord]))
migrationResult = config.storeSchema.migrateStoreSnapshot(storeSnapshot, data.schema)
migrationResult = store.schema.migrateStoreSnapshot(storeSnapshot, data.schema)
} catch (e) {
// junk data in the migration
return Result.err({ type: 'invalidRecords', cause: e })
@ -137,7 +137,12 @@ export function parseTldrawJsonFile({
// we should be able to validate them. if any of the records at this stage
// are invalid, we don't open the file
try {
return Result.ok(config.createStore({ initialData: migrationResult.value, instanceId }))
return Result.ok(
createTLStore({
initialData: migrationResult.value,
instanceId,
})
)
} catch (e) {
// junk data in the records (they're not validated yet!) could cause the
// migrations to crash. We treat any throw from a migration as an
@ -205,7 +210,7 @@ export async function parseAndLoadDocument(
forceDarkMode?: boolean
) {
const parseFileResult = parseTldrawJsonFile({
config: new TldrawEditorConfig(),
store: createTLStore(),
json: document,
instanceId: app.instanceId,
})

Wyświetl plik

@ -1,11 +1,11 @@
import { createCustomShapeId, InstanceRecordType, TldrawEditorConfig } from '@tldraw/editor'
import { createCustomShapeId, createTLStore, InstanceRecordType, TLStore } from '@tldraw/editor'
import { MigrationFailureReason, UnknownRecord } from '@tldraw/tlstore'
import { assert } from '@tldraw/utils'
import { parseTldrawJsonFile as _parseTldrawJsonFile, TldrawFile } from '../lib/file'
const parseTldrawJsonFile = (config: TldrawEditorConfig, json: string) =>
const parseTldrawJsonFile = (store: TLStore, json: string) =>
_parseTldrawJsonFile({
config,
store,
json,
instanceId: InstanceRecordType.createCustomId('instance'),
})
@ -16,26 +16,26 @@ function serialize(file: TldrawFile): string {
describe('parseTldrawJsonFile', () => {
it('returns an error if the file is not json', () => {
const result = parseTldrawJsonFile(new TldrawEditorConfig(), 'not json')
const store = createTLStore()
const result = parseTldrawJsonFile(store, 'not json')
assert(!result.ok)
expect(result.error.type).toBe('notATldrawFile')
})
it("returns an error if the file doesn't look like a tldraw file", () => {
const result = parseTldrawJsonFile(
new TldrawEditorConfig(),
JSON.stringify({ not: 'a tldraw file' })
)
const store = createTLStore()
const result = parseTldrawJsonFile(store, JSON.stringify({ not: 'a tldraw file' }))
assert(!result.ok)
expect(result.error.type).toBe('notATldrawFile')
})
it('returns an error if the file version is too old', () => {
const store = createTLStore()
const result = parseTldrawJsonFile(
new TldrawEditorConfig(),
store,
serialize({
tldrawFileFormatVersion: 0,
schema: new TldrawEditorConfig().storeSchema.serialize(),
schema: store.schema.serialize(),
records: [],
})
)
@ -44,11 +44,12 @@ describe('parseTldrawJsonFile', () => {
})
it('returns an error if the file version is too new', () => {
const store = createTLStore()
const result = parseTldrawJsonFile(
new TldrawEditorConfig(),
store,
serialize({
tldrawFileFormatVersion: 100,
schema: new TldrawEditorConfig().storeSchema.serialize(),
schema: store.schema.serialize(),
records: [],
})
)
@ -57,10 +58,11 @@ describe('parseTldrawJsonFile', () => {
})
it('returns an error if migrations fail', () => {
const serializedSchema = new TldrawEditorConfig().storeSchema.serialize()
const store = createTLStore()
const serializedSchema = store.schema.serialize()
serializedSchema.storeVersion = 100
const result = parseTldrawJsonFile(
new TldrawEditorConfig(),
store,
serialize({
tldrawFileFormatVersion: 1,
schema: serializedSchema,
@ -71,10 +73,11 @@ describe('parseTldrawJsonFile', () => {
assert(result.error.type === 'migrationFailed')
expect(result.error.reason).toBe(MigrationFailureReason.TargetVersionTooOld)
const serializedSchema2 = new TldrawEditorConfig().storeSchema.serialize()
const store2 = createTLStore()
const serializedSchema2 = store2.schema.serialize()
serializedSchema2.recordVersions.shape.version = 100
const result2 = parseTldrawJsonFile(
new TldrawEditorConfig(),
store2,
serialize({
tldrawFileFormatVersion: 1,
schema: serializedSchema2,
@ -88,11 +91,12 @@ describe('parseTldrawJsonFile', () => {
})
it('returns an error if a record is invalid', () => {
const store = createTLStore()
const result = parseTldrawJsonFile(
new TldrawEditorConfig(),
store,
serialize({
tldrawFileFormatVersion: 1,
schema: new TldrawEditorConfig().storeSchema.serialize(),
schema: store.schema.serialize(),
records: [
{
typeName: 'shape',
@ -103,19 +107,21 @@ describe('parseTldrawJsonFile', () => {
],
})
)
assert(!result.ok)
assert(result.error.type === 'invalidRecords')
expect(result.error.cause).toMatchInlineSnapshot(
`[ValidationError: At shape(id = shape:shape, type = geo).rotation: Expected number, got undefined]`
`[ValidationError: At shape(id = shape:shape, type = geo).x: Expected number, got undefined]`
)
})
it('returns a store if the file is valid', () => {
const store = createTLStore()
const result = parseTldrawJsonFile(
new TldrawEditorConfig(),
store,
serialize({
tldrawFileFormatVersion: 1,
schema: new TldrawEditorConfig().storeSchema.serialize(),
schema: store.schema.serialize(),
records: [],
})
)

Wyświetl plik

@ -5,18 +5,13 @@
```ts
import { TldrawEditorProps } from '@tldraw/editor';
import { TldrawUiContextProviderProps } from '@tldraw/ui';
import { TldrawUiProps } from '@tldraw/ui';
// @public (undocumented)
export function Tldraw(props: Omit<TldrawEditorProps, 'config' | 'store'> & TldrawUiContextProviderProps & {
persistenceKey?: string;
hideUi?: boolean;
config?: TldrawEditorProps['config'];
}): JSX.Element;
export function Tldraw(props: TldrawEditorProps & TldrawUiProps): JSX.Element;
export * from "@tldraw/editor";
export * from "@tldraw/tlsync-client";
export * from "@tldraw/ui";
// (No @packageDocumentation comment for this package)

Wyświetl plik

@ -47,7 +47,6 @@
"dependencies": {
"@tldraw/editor": "workspace:*",
"@tldraw/polyfills": "workspace:*",
"@tldraw/tlsync-client": "workspace:*",
"@tldraw/ui": "workspace:*"
},
"peerDependencies": {

Wyświetl plik

@ -4,7 +4,5 @@ import '@tldraw/polyfills'
// eslint-disable-next-line local/no-export-star
export * from '@tldraw/editor'
// eslint-disable-next-line local/no-export-star
export * from '@tldraw/tlsync-client'
// eslint-disable-next-line local/no-export-star
export * from '@tldraw/ui'
export { Tldraw } from './lib/Tldraw'

Wyświetl plik

@ -1,38 +1,12 @@
import { Canvas, TldrawEditor, TldrawEditorConfig, TldrawEditorProps } from '@tldraw/editor'
import { DEFAULT_DOCUMENT_NAME, TAB_ID, useLocalSyncClient } from '@tldraw/tlsync-client'
import { ContextMenu, TldrawUi, TldrawUiContextProviderProps } from '@tldraw/ui'
import { useMemo } from 'react'
import { Canvas, TldrawEditor, TldrawEditorProps } from '@tldraw/editor'
import { ContextMenu, TldrawUi, TldrawUiProps } from '@tldraw/ui'
/** @public */
export function Tldraw(
props: Omit<TldrawEditorProps, 'store' | 'config'> &
TldrawUiContextProviderProps & {
/** The key under which to persist this editor's data to local storage. */
persistenceKey?: string
/** Whether to hide the user interface and only display the canvas. */
hideUi?: boolean
/** A custom configuration for this Tldraw editor */
config?: TldrawEditorProps['config']
}
) {
const {
config,
children,
persistenceKey = DEFAULT_DOCUMENT_NAME,
instanceId = TAB_ID,
...rest
} = props
const _config = useMemo(() => config ?? new TldrawEditorConfig(), [config])
const syncedStore = useLocalSyncClient({
instanceId,
config: _config,
universalPersistenceKey: persistenceKey,
})
export function Tldraw(props: TldrawEditorProps & TldrawUiProps) {
const { children, ...rest } = props
return (
<TldrawEditor {...rest} instanceId={instanceId} store={syncedStore} config={_config}>
<TldrawEditor {...rest}>
<TldrawUi {...rest}>
<ContextMenu>
<Canvas />

Wyświetl plik

@ -8,5 +8,5 @@
"noImplicitReturns": false,
"rootDir": "src"
},
"references": [{ "path": "../editor" }, { "path": "../tlsync-client" }, { "path": "../ui" }]
"references": [{ "path": "../editor" }, { "path": "../ui" }]
}

Wyświetl plik

@ -118,8 +118,8 @@ export function createShapeValidator<Type extends string, Props extends object>(
}>;
// @public
export function createTLSchema<T extends TLUnknownShape>(opts?: {
customShapes?: { [K in T["type"]]: CustomShapeInfo<T>; } | undefined;
export function createTLSchema(opts?: {
customShapes: Record<string, SchemaShapeInfo>;
}): StoreSchema<TLRecord, TLStoreProps>;
// @public (undocumented)
@ -376,7 +376,7 @@ export const groupShapeTypeValidator: T.Validator<TLGroupShape>;
export const handleTypeValidator: T.Validator<TLHandle>;
// @public (undocumented)
export const highlightShapeMigrations: Migrations;
export const highlightShapeTypeMigrations: Migrations;
// @public (undocumented)
export const highlightShapeTypeValidator: T.Validator<TLHighlightShape>;
@ -477,6 +477,14 @@ export const pointerTypeValidator: T.Validator<TLPointer>;
// @internal (undocumented)
export const rootShapeTypeMigrations: Migrations;
// @public (undocumented)
export type SchemaShapeInfo = {
migrations?: Migrations;
validator?: {
validate: (record: any) => any;
};
};
// @public (undocumented)
export const scribbleTypeValidator: T.Validator<TLScribble>;

Wyświetl plik

@ -10,7 +10,7 @@ import { InstancePageStateRecordType } from './records/TLInstancePageState'
import { InstancePresenceRecordType } from './records/TLInstancePresence'
import { PageRecordType } from './records/TLPage'
import { PointerRecordType } from './records/TLPointer'
import { TLShape, TLUnknownShape, rootShapeTypeMigrations } from './records/TLShape'
import { TLShape, rootShapeTypeMigrations } from './records/TLShape'
import { UserDocumentRecordType } from './records/TLUserDocument'
import { storeMigrations } from './schema'
import { arrowShapeTypeMigrations, arrowShapeTypeValidator } from './shapes/TLArrowShape'
@ -20,92 +20,122 @@ import { embedShapeTypeMigrations, embedShapeTypeValidator } from './shapes/TLEm
import { frameShapeTypeMigrations, frameShapeTypeValidator } from './shapes/TLFrameShape'
import { geoShapeTypeMigrations, geoShapeTypeValidator } from './shapes/TLGeoShape'
import { groupShapeTypeMigrations, groupShapeTypeValidator } from './shapes/TLGroupShape'
import { highlightShapeMigrations, highlightShapeTypeValidator } from './shapes/TLHighlightShape'
import {
highlightShapeTypeMigrations,
highlightShapeTypeValidator,
} from './shapes/TLHighlightShape'
import { imageShapeTypeMigrations, imageShapeTypeValidator } from './shapes/TLImageShape'
import { lineShapeTypeMigrations, lineShapeTypeValidator } from './shapes/TLLineShape'
import { noteShapeTypeMigrations, noteShapeTypeValidator } from './shapes/TLNoteShape'
import { textShapeTypeMigrations, textShapeTypeValidator } from './shapes/TLTextShape'
import { videoShapeTypeMigrations, videoShapeTypeValidator } from './shapes/TLVideoShape'
type DefaultShapeInfo<T extends TLShape> = {
validator: T.Validator<T>
migrations: Migrations
/** @public */
export type SchemaShapeInfo = {
migrations?: Migrations
validator?: { validate: (record: any) => any }
}
const DEFAULT_SHAPES: { [K in TLShape['type']]: DefaultShapeInfo<Extract<TLShape, { type: K }>> } =
{
arrow: { migrations: arrowShapeTypeMigrations, validator: arrowShapeTypeValidator },
bookmark: { migrations: bookmarkShapeTypeMigrations, validator: bookmarkShapeTypeValidator },
draw: { migrations: drawShapeTypeMigrations, validator: drawShapeTypeValidator },
embed: { migrations: embedShapeTypeMigrations, validator: embedShapeTypeValidator },
frame: { migrations: frameShapeTypeMigrations, validator: frameShapeTypeValidator },
geo: { migrations: geoShapeTypeMigrations, validator: geoShapeTypeValidator },
group: { migrations: groupShapeTypeMigrations, validator: groupShapeTypeValidator },
image: { migrations: imageShapeTypeMigrations, validator: imageShapeTypeValidator },
line: { migrations: lineShapeTypeMigrations, validator: lineShapeTypeValidator },
note: { migrations: noteShapeTypeMigrations, validator: noteShapeTypeValidator },
text: { migrations: textShapeTypeMigrations, validator: textShapeTypeValidator },
video: { migrations: videoShapeTypeMigrations, validator: videoShapeTypeValidator },
highlight: { migrations: highlightShapeMigrations, validator: highlightShapeTypeValidator },
}
const coreShapes: Record<string, SchemaShapeInfo> = {
group: {
migrations: groupShapeTypeMigrations,
validator: groupShapeTypeValidator,
},
bookmark: {
migrations: bookmarkShapeTypeMigrations,
validator: bookmarkShapeTypeValidator,
},
embed: {
migrations: embedShapeTypeMigrations,
validator: embedShapeTypeValidator,
},
image: {
migrations: imageShapeTypeMigrations,
validator: imageShapeTypeValidator,
},
text: {
migrations: textShapeTypeMigrations,
validator: textShapeTypeValidator,
},
video: {
migrations: videoShapeTypeMigrations,
validator: videoShapeTypeValidator,
},
}
type CustomShapeInfo<T extends TLUnknownShape> = {
validator?: { validate: (record: T) => T }
migrations?: Migrations
const defaultShapes: Record<string, SchemaShapeInfo> = {
arrow: {
migrations: arrowShapeTypeMigrations,
validator: arrowShapeTypeValidator,
},
draw: {
migrations: drawShapeTypeMigrations,
validator: drawShapeTypeValidator,
},
frame: {
migrations: frameShapeTypeMigrations,
validator: frameShapeTypeValidator,
},
geo: {
migrations: geoShapeTypeMigrations,
validator: geoShapeTypeValidator,
},
line: {
migrations: lineShapeTypeMigrations,
validator: lineShapeTypeValidator,
},
note: {
migrations: noteShapeTypeMigrations,
validator: noteShapeTypeValidator,
},
highlight: {
migrations: highlightShapeTypeMigrations,
validator: highlightShapeTypeValidator,
},
}
/**
* Create a store schema for a tldraw store that includes all the default shapes together with any custom shapes.
* @public */
export function createTLSchema<T extends TLUnknownShape>(
* Create a TLSchema with custom shapes. Custom shapes cannot override default shapes.
*
* @param opts - Options
*
* @public */
export function createTLSchema(
opts = {} as {
customShapes?: { [K in T['type']]: CustomShapeInfo<T> }
customShapes: Record<string, SchemaShapeInfo>
}
) {
const { customShapes = {} } = opts
const { customShapes } = opts
const defaultShapeSubTypeEntries = Object.entries(DEFAULT_SHAPES) as [
TLShape['type'],
DefaultShapeInfo<TLShape>
][]
const customShapeSubTypeEntries = Object.entries(customShapes) as [
T['type'],
CustomShapeInfo<T>
][]
// Create a shape record that incorporates the default shapes and any custom shapes
// into its subtype migrations and validators, so that we can migrate any new custom
// subtypes. Note that migrations AND validators for custom shapes are optional. If
// not provided, we use an empty migrations set and/or an "any" validator.
const shapeSubTypeMigrationsWithCustomSubTypeMigrations = {
...Object.fromEntries(defaultShapeSubTypeEntries.map(([k, v]) => [k, v.migrations])),
...Object.fromEntries(
customShapeSubTypeEntries.map(([k, v]) => [k, v.migrations ?? defineMigrations({})])
),
for (const key in customShapes) {
if (key in coreShapes) {
throw Error(`Can't override default shape ${key}!`)
}
}
const validatorWithCustomShapeValidators = T.model(
'shape',
T.union('type', {
...Object.fromEntries(defaultShapeSubTypeEntries.map(([k, v]) => [k, v.validator])),
...Object.fromEntries(
customShapeSubTypeEntries.map(([k, v]) => [k, (v.validator as T.Validator<any>) ?? T.any])
),
})
)
const allShapeEntries = Object.entries({ ...coreShapes, ...defaultShapes, ...customShapes })
const shapeRecord = createRecordType<TLShape>('shape', {
const ShapeRecordType = createRecordType<TLShape>('shape', {
migrations: defineMigrations({
currentVersion: rootShapeTypeMigrations.currentVersion,
firstVersion: rootShapeTypeMigrations.firstVersion,
migrators: rootShapeTypeMigrations.migrators,
subTypeKey: 'type',
subTypeMigrations: shapeSubTypeMigrationsWithCustomSubTypeMigrations,
subTypeMigrations: {
...Object.fromEntries(
allShapeEntries.map(([k, v]) => [k, v.migrations ?? defineMigrations({})])
),
},
}),
validator: validatorWithCustomShapeValidators,
scope: 'document',
validator: T.model(
'shape',
T.union('type', {
...Object.fromEntries(
allShapeEntries.map(([k, v]) => [k, (v.validator as T.Validator<any>) ?? T.any])
),
})
),
}).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false }))
return StoreSchema.create<TLRecord, TLStoreProps>(
@ -116,7 +146,7 @@ export function createTLSchema<T extends TLUnknownShape>(
instance: InstanceRecordType,
instance_page_state: InstancePageStateRecordType,
page: PageRecordType,
shape: shapeRecord,
shape: ShapeRecordType,
user_document: UserDocumentRecordType,
instance_presence: InstancePresenceRecordType,
pointer: PointerRecordType,

Wyświetl plik

@ -24,7 +24,7 @@ export {
} from './assets/TLVideoAsset'
export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation'
export { createPresenceStateDerivation } from './createPresenceStateDerivation'
export { createTLSchema } from './createTLSchema'
export { createTLSchema, type SchemaShapeInfo } from './createTLSchema'
export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup'
export { type Box2dModel, type Vec2dModel } from './geometry-types'
export {
@ -157,7 +157,7 @@ export {
type TLGroupShapeProps,
} from './shapes/TLGroupShape'
export {
highlightShapeMigrations,
highlightShapeTypeMigrations,
highlightShapeTypeValidator,
type TLHighlightShape,
type TLHighlightShapeProps,

Wyświetl plik

@ -18,7 +18,6 @@ export type TLHighlightShapeProps = {
/** @public */
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
// --- VALIDATION ---
/** @public */
export const highlightShapeTypeValidator: T.Validator<TLHighlightShape> = createShapeValidator(
'highlight',
@ -32,6 +31,5 @@ export const highlightShapeTypeValidator: T.Validator<TLHighlightShape> = create
})
)
// --- MIGRATIONS ---
/** @public */
export const highlightShapeMigrations = defineMigrations({})
export const highlightShapeTypeMigrations = defineMigrations({})

Wyświetl plik

@ -1,200 +0,0 @@
# v2.0.0-alpha.12 (Mon Apr 03 2023)
#### 🐛 Bug Fix
- Make sure all types and build stuff get run in CI [#1548](https://github.com/tldraw/tldraw-lite/pull/1548) ([@SomeHats](https://github.com/SomeHats))
- add pre-commit api report generation [#1517](https://github.com/tldraw/tldraw-lite/pull/1517) ([@SomeHats](https://github.com/SomeHats))
- [chore] restore api extractor [#1500](https://github.com/tldraw/tldraw-lite/pull/1500) ([@steveruizok](https://github.com/steveruizok))
- Remove initial data parameter as it is not being used. [#1480](https://github.com/tldraw/tldraw-lite/pull/1480) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- David/publish good [#1488](https://github.com/tldraw/tldraw-lite/pull/1488) ([@ds300](https://github.com/ds300))
- [chore] alpha 10 [#1486](https://github.com/tldraw/tldraw-lite/pull/1486) ([@ds300](https://github.com/ds300))
- [chore] package build improvements [#1484](https://github.com/tldraw/tldraw-lite/pull/1484) ([@ds300](https://github.com/ds300))
- [chore] bump for alpha 8 [#1485](https://github.com/tldraw/tldraw-lite/pull/1485) ([@steveruizok](https://github.com/steveruizok))
- stop using broken-af turbo for publishing [#1476](https://github.com/tldraw/tldraw-lite/pull/1476) ([@ds300](https://github.com/ds300))
- [chore] add canary release script [#1423](https://github.com/tldraw/tldraw-lite/pull/1423) ([@ds300](https://github.com/ds300) [@steveruizok](https://github.com/steveruizok))
- [chore] upgrade yarn [#1430](https://github.com/tldraw/tldraw-lite/pull/1430) ([@ds300](https://github.com/ds300))
- [update] docs [#1448](https://github.com/tldraw/tldraw-lite/pull/1448) ([@steveruizok](https://github.com/steveruizok))
- [fix] dev version number for tldraw/tldraw [#1434](https://github.com/tldraw/tldraw-lite/pull/1434) ([@steveruizok](https://github.com/steveruizok))
- repo cleanup [#1426](https://github.com/tldraw/tldraw-lite/pull/1426) ([@steveruizok](https://github.com/steveruizok))
- Vscode extension [#1253](https://github.com/tldraw/tldraw-lite/pull/1253) ([@steveruizok](https://github.com/steveruizok) [@MitjaBezensek](https://github.com/MitjaBezensek) [@orangemug](https://github.com/orangemug))
- Run all the tests. Fix linting for tests. [#1389](https://github.com/tldraw/tldraw-lite/pull/1389) ([@MitjaBezensek](https://github.com/MitjaBezensek))
- add beta-redirect app [#1415](https://github.com/tldraw/tldraw-lite/pull/1415) ([@SomeHats](https://github.com/SomeHats))
#### Authors: 5
- alex ([@SomeHats](https://github.com/SomeHats))
- David Sheldrick ([@ds300](https://github.com/ds300))
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Orange Mug ([@orangemug](https://github.com/orangemug))
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
---
# @tldraw/tlsync-client
## 2.0.0-alpha.11
### Patch Changes
- fix some package build scripting
- Updated dependencies
- @tldraw/editor@2.0.0-alpha.11
- @tldraw/tlstore@2.0.0-alpha.11
## 2.0.0-alpha.10
### Patch Changes
- 4b4399b6e: redeploy with yarn to prevent package version issues
- Updated dependencies [4b4399b6e]
- @tldraw/tlstore@2.0.0-alpha.10
- @tldraw/editor@2.0.0-alpha.10
## 2.0.0-alpha.9
### Patch Changes
- Release day!
- Updated dependencies
- @tldraw/editor@2.0.0-alpha.9
- @tldraw/tlstore@2.0.0-alpha.9
## 2.0.0-alpha.8
### Patch Changes
- 23dd81cfe: Make signia a peer dependency
- Updated dependencies [23dd81cfe]
- @tldraw/editor@2.0.0-alpha.8
- @tldraw/tlstore@2.0.0-alpha.8
- @tldraw/tlsync@2.0.0-alpha.8
## 2.0.0-alpha.7
### Patch Changes
- Bug fixes.
- Updated dependencies
- @tldraw/editor@2.0.0-alpha.7
- @tldraw/tlstore@2.0.0-alpha.7
- @tldraw/tlsync@2.0.0-alpha.7
## 2.0.0-alpha.6
### Patch Changes
- Add licenses.
- Updated dependencies
- @tldraw/editor@2.0.0-alpha.6
- @tldraw/tlstore@2.0.0-alpha.6
- @tldraw/tlsync@2.0.0-alpha.6
## 2.0.0-alpha.5
### Patch Changes
- Add CSS files to tldraw/tldraw.
- Updated dependencies
- @tldraw/editor@2.0.0-alpha.5
- @tldraw/tlstore@2.0.0-alpha.5
- @tldraw/tlsync@2.0.0-alpha.5
## 2.0.0-alpha.4
### Patch Changes
- Add children to tldraw/tldraw
- Updated dependencies
- @tldraw/editor@2.0.0-alpha.4
- @tldraw/tlstore@2.0.0-alpha.4
- @tldraw/tlsync@2.0.0-alpha.4
## 2.0.0-alpha.3
### Patch Changes
- Change permissions.
- Updated dependencies
- @tldraw/editor@2.0.0-alpha.3
- @tldraw/tlstore@2.0.0-alpha.3
- @tldraw/tlsync@2.0.0-alpha.3
## 2.0.0-alpha.2
### Patch Changes
- Add tldraw, editor
- Updated dependencies
- @tldraw/editor@2.0.0-alpha.2
- @tldraw/tlstore@2.0.0-alpha.2
- @tldraw/tlsync@2.0.0-alpha.2
## 0.1.0-alpha.11
### Patch Changes
- Fix stale reactors.
- Updated dependencies
- @tldraw/tldraw-beta@0.1.0-alpha.11
- @tldraw/tlstore@0.1.0-alpha.11
- @tldraw/tlsync@0.1.0-alpha.11
## 0.1.0-alpha.10
### Patch Changes
- Fix type export bug.
- Updated dependencies
- @tldraw/tldraw-beta@0.1.0-alpha.10
- @tldraw/tlstore@0.1.0-alpha.10
- @tldraw/tlsync@0.1.0-alpha.10
## 0.1.0-alpha.9
### Patch Changes
- Fix import bugs.
- Updated dependencies
- @tldraw/tldraw-beta@0.1.0-alpha.9
- @tldraw/tlstore@0.1.0-alpha.9
- @tldraw/tlsync@0.1.0-alpha.9
## 0.1.0-alpha.8
### Patch Changes
- Changes validation requirements, exports validation helpers.
- Updated dependencies
- @tldraw/tldraw-beta@0.1.0-alpha.8
- @tldraw/tlstore@0.1.0-alpha.8
- @tldraw/tlsync@0.1.0-alpha.8
## 0.1.0-alpha.7
### Patch Changes
- - Pre-pre-release update
- Updated dependencies
- @tldraw/tldraw-beta@0.1.0-alpha.7
- @tldraw/tlstore@0.1.0-alpha.7
- @tldraw/tlsync@0.1.0-alpha.7
## 0.0.2-alpha.1
### Patch Changes
- Fix error with HMR
- Updated dependencies
- @tldraw/tldraw-beta@0.0.2-alpha.1
- @tldraw/tlstore@0.0.2-alpha.1
- @tldraw/tlsync@0.0.2-alpha.1
## 0.0.2-alpha.0
### Patch Changes
- Initial release
- Updated dependencies
- @tldraw/tldraw-beta@0.0.2-alpha.0
- @tldraw/tlstore@0.0.2-alpha.0
- @tldraw/tlsync@0.0.2-alpha.0

Wyświetl plik

@ -1,190 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2023 tldraw GB Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Wyświetl plik

@ -1,5 +0,0 @@
# @tldraw/tlsync-client
## License
The source code in this repository (as well as our 2.0+ distributions and releases) are currently licensed under Apache-2.0. These licenses are subject to change in our upcoming 2.0 release. If you are planning to use tldraw in a commercial product, please reach out at [hello@tldraw.com](mailto://hello@tldraw.com).

Wyświetl plik

@ -1,4 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"extends": "../../config/api-extractor.json"
}

Wyświetl plik

@ -1,34 +0,0 @@
## API Report File for "@tldraw/tlsync-client"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { SyncedStore } from '@tldraw/editor';
import { TldrawEditorConfig } from '@tldraw/editor';
import { TLInstanceId } from '@tldraw/editor';
// @public (undocumented)
export const DEFAULT_DOCUMENT_NAME: any;
// @public
export function hardReset({ shouldReload }?: {
shouldReload?: boolean | undefined;
}): Promise<void>;
// @public (undocumented)
export const STORE_PREFIX = "TLDRAW_DOCUMENT_v2";
// @public (undocumented)
export const TAB_ID: TLInstanceId;
// @public
export function useLocalSyncClient({ universalPersistenceKey, instanceId, config, }: {
universalPersistenceKey: string;
instanceId: TLInstanceId;
config: TldrawEditorConfig;
}): SyncedStore;
// (No @packageDocumentation comment for this package)
```

Wyświetl plik

@ -1,75 +0,0 @@
{
"name": "@tldraw/tlsync-client",
"description": "A tiny little drawing app (multiplayer sync).",
"version": "2.0.0-alpha.12",
"packageManager": "yarn@3.5.0",
"author": {
"name": "tldraw GB Ltd.",
"email": "hello@tldraw.com"
},
"homepage": "https://tldraw.dev",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/tldraw/tldraw"
},
"bugs": {
"url": "https://github.com/tldraw/tldraw/issues"
},
"keywords": [
"tldraw",
"drawing",
"app",
"development",
"whiteboard",
"canvas",
"infinite"
],
"/* NOTE */": "These `main` and `types` fields are rewritten by the build script. They are not the actual values we publish",
"main": "./src/index.ts",
"types": "./.tsbuild/index.d.ts",
"/* GOTCHA */": "files will include ./dist and index.d.ts by default, add any others you want to include in here",
"files": [],
"scripts": {
"test": "lazy inherit",
"test-coverage": "lazy inherit",
"build": "yarn run -T tsx ../../scripts/build-package.ts",
"build-api": "yarn run -T tsx ../../scripts/build-api.ts",
"prepack": "yarn run -T tsx ../../scripts/prepack.ts",
"postpack": "../../scripts/postpack.sh",
"pack-tarball": "yarn pack",
"lint": "yarn run -T tsx ../../scripts/lint.ts"
},
"devDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"lazyrepo": "0.0.0-alpha.26",
"ws": "^8.10.0"
},
"optionalDependencies": {
"react": "*"
},
"jest": {
"preset": "config/jest/node",
"testEnvironment": "jsdom",
"setupFiles": [
"./setupJest.js"
],
"moduleNameMapper": {
"^~(.*)": "<rootDir>/src/$1"
},
"transformIgnorePatterns": [
"node_modules/(?!(nanoid|escape-string-regexp)/)"
]
},
"peerDependencies": {
"signia": "*",
"signia-react": "*"
},
"dependencies": {
"@tldraw/editor": "workspace:*",
"@tldraw/tlstore": "workspace:*",
"@tldraw/utils": "workspace:*",
"idb": "^7.1.0"
}
}

Wyświetl plik

@ -1,10 +0,0 @@
window.crypto = {
// required by nanoid
// if we need more of the crypto apis, just add a proper mock here
getRandomValues: function (array) {
for (var i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256)
}
return array
},
}

Wyświetl plik

@ -1,3 +0,0 @@
export { hardReset } from './lib/hardReset'
export { useLocalSyncClient } from './lib/hooks/useLocalSyncClient'
export { DEFAULT_DOCUMENT_NAME, STORE_PREFIX, TAB_ID } from './lib/persistence-constants'

Wyświetl plik

@ -1,56 +0,0 @@
import { SyncedStore, TldrawEditorConfig, TLInstanceId, uniqueId } from '@tldraw/editor'
import { useEffect, useState } from 'react'
import '../hardReset'
import { TLLocalSyncClient } from '../TLLocalSyncClient'
/**
* Use a client that persists to indexedDB and syncs to other stores with the same instance id, e.g. other tabs running the same instance of tldraw.
*
* @public
*/
export function useLocalSyncClient({
universalPersistenceKey,
instanceId,
config,
}: {
universalPersistenceKey: string
instanceId: TLInstanceId
config: TldrawEditorConfig
}): SyncedStore {
const [state, setState] = useState<{ id: string; syncedStore: SyncedStore } | null>(null)
useEffect(() => {
const id = uniqueId()
setState({
id,
syncedStore: { status: 'loading' },
})
const setSyncedStore = (syncedStore: SyncedStore) => {
setState((prev) => {
if (prev?.id === id) {
return { id, syncedStore }
}
return prev
})
}
const store = config.createStore({ instanceId })
const client = new TLLocalSyncClient(store, {
universalPersistenceKey,
onLoad() {
setSyncedStore({ status: 'synced', store })
},
onLoadError(err) {
setSyncedStore({ status: 'error', error: err })
},
})
return () => {
setState((prevState) => (prevState?.id === id ? null : prevState))
client.close()
}
}, [instanceId, universalPersistenceKey, config])
return state?.syncedStore ?? { status: 'loading' }
}

Wyświetl plik

@ -1,10 +0,0 @@
{
"extends": "../../config/tsconfig.base.json",
"include": ["src", "start.ts"],
"exclude": ["node_modules", "dist", "docs", ".tsbuild*"],
"compilerOptions": {
"outDir": "./.tsbuild",
"rootDir": "src"
},
"references": [{ "path": "../tlstore" }, { "path": "../editor" }]
}

Wyświetl plik

@ -621,10 +621,10 @@ export interface TLDialog {
// @public (undocumented)
export const TldrawUi: React_2.NamedExoticComponent<{
shareZone?: ReactNode;
renderDebugMenuItems?: (() => React_2.ReactNode) | undefined;
children?: ReactNode;
hideUi?: boolean | undefined;
shareZone?: ReactNode;
renderDebugMenuItems?: (() => React_2.ReactNode) | undefined;
} & TldrawUiContextProviderProps>;
// @public (undocumented)
@ -667,6 +667,14 @@ export interface TldrawUiOverrides {
translations?: TranslationProviderProps['overrides'];
}
// @public (undocumented)
export type TldrawUiProps = {
children?: ReactNode;
hideUi?: boolean;
shareZone?: ReactNode;
renderDebugMenuItems?: () => React_2.ReactNode;
} & TldrawUiContextProviderProps;
// @public (undocumented)
export type TLListedTranslation = {
readonly locale: string;

Wyświetl plik

@ -55,7 +55,6 @@
"@tldraw/editor": "workspace:*",
"@tldraw/primitives": "workspace:*",
"@tldraw/tlschema": "workspace:*",
"@tldraw/tlsync-client": "workspace:*",
"@tldraw/utils": "workspace:*",
"browser-fs-access": "^0.31.0",
"classnames": "^2.3.2",

Wyświetl plik

@ -1,7 +1,7 @@
import * as Dialog from './lib/components/primitives/Dialog'
import * as DropdownMenu from './lib/components/primitives/DropdownMenu'
export { TldrawUi, TldrawUiContent } from './lib/TldrawUi'
export { TldrawUi, TldrawUiContent, type TldrawUiProps } from './lib/TldrawUi'
export {
TldrawUiContextProvider,
type TldrawUiContextProviderProps,

Wyświetl plik

@ -24,6 +24,17 @@ import { useNativeClipboardEvents } from './hooks/useClipboardEvents'
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
import { useTranslation } from './hooks/useTranslation/useTranslation'
/** @public */
export type TldrawUiProps = {
children?: ReactNode
/** Whether to hide the interface and only display the canvas. */
hideUi?: boolean
/** A component to use for the share zone (will be deprecated) */
shareZone?: ReactNode
/** Additional items to add to the debug menu (will be deprecated)*/
renderDebugMenuItems?: () => React.ReactNode
} & TldrawUiContextProviderProps
/**
* @public
*/
@ -33,13 +44,7 @@ export const TldrawUi = React.memo(function TldrawUi({
children,
hideUi,
...rest
}: {
shareZone?: ReactNode
renderDebugMenuItems?: () => React.ReactNode
children?: ReactNode
/** Whether to hide the interface and only display the canvas. */
hideUi?: boolean
} & TldrawUiContextProviderProps) {
}: TldrawUiProps) {
return (
<TldrawUiContextProvider {...rest}>
<TldrawUiInner
@ -64,12 +69,6 @@ const TldrawUiInner = React.memo(function TldrawUiInner({
hideUi,
...rest
}: TldrawUiContentProps & { children: ReactNode }) {
// const isLoaded = usePreloadIcons()
// if (!isLoaded) {
// return <LoadingScreen>Loading assets...</LoadingScreen>
// }
// The hideUi prop should prevent the UI from mounting.
// If we ever need want the UI to mount and preserve state, then
// we should change this behavior and hide the UI via CSS instead.

Wyświetl plik

@ -8,10 +8,5 @@
"noImplicitReturns": false,
"rootDir": "src"
},
"references": [
{ "path": "../editor" },
{ "path": "../primitives" },
{ "path": "../tlsync-client" },
{ "path": "../utils" }
]
"references": [{ "path": "../editor" }, { "path": "../primitives" }, { "path": "../utils" }]
}

Wyświetl plik

@ -4297,7 +4297,6 @@ __metadata:
"@tldraw/tldraw": "workspace:*"
"@tldraw/tlschema": "workspace:*"
"@tldraw/tlstore": "workspace:*"
"@tldraw/tlsync-client": "workspace:*"
"@tldraw/tlvalidate": "workspace:*"
"@tldraw/ui": "workspace:*"
"@tldraw/utils": "workspace:*"
@ -4351,8 +4350,9 @@ __metadata:
escape-string-regexp: ^5.0.0
eventemitter3: ^4.0.7
fake-indexeddb: ^4.0.0
idb: ^7.1.1
is-plain-object: ^5.0.0
jest-canvas-mock: ^2.4.0
jest-canvas-mock: ^2.5.1
jest-environment-jsdom: ^29.4.3
lazyrepo: 0.0.0-alpha.26
lodash.throttle: ^4.1.1
@ -4477,7 +4477,6 @@ __metadata:
"@testing-library/react": ^14.0.0
"@tldraw/editor": "workspace:*"
"@tldraw/polyfills": "workspace:*"
"@tldraw/tlsync-client": "workspace:*"
"@tldraw/ui": "workspace:*"
chokidar-cli: ^3.0.0
jest-canvas-mock: ^2.4.0
@ -4521,28 +4520,6 @@ __metadata:
languageName: unknown
linkType: soft
"@tldraw/tlsync-client@workspace:*, @tldraw/tlsync-client@workspace:packages/tlsync-client":
version: 0.0.0-use.local
resolution: "@tldraw/tlsync-client@workspace:packages/tlsync-client"
dependencies:
"@tldraw/editor": "workspace:*"
"@tldraw/tlstore": "workspace:*"
"@tldraw/utils": "workspace:*"
"@types/react": "*"
"@types/react-dom": "*"
idb: ^7.1.0
lazyrepo: 0.0.0-alpha.26
react: "*"
ws: ^8.10.0
peerDependencies:
signia: "*"
signia-react: "*"
dependenciesMeta:
react:
optional: true
languageName: unknown
linkType: soft
"@tldraw/tlvalidate@workspace:*, @tldraw/tlvalidate@workspace:packages/tlvalidate":
version: 0.0.0-use.local
resolution: "@tldraw/tlvalidate@workspace:packages/tlvalidate"
@ -4570,7 +4547,6 @@ __metadata:
"@tldraw/editor": "workspace:*"
"@tldraw/primitives": "workspace:*"
"@tldraw/tlschema": "workspace:*"
"@tldraw/tlsync-client": "workspace:*"
"@tldraw/utils": "workspace:*"
"@types/lz-string": ^1.3.34
browser-fs-access: ^0.31.0
@ -4606,7 +4582,6 @@ __metadata:
"@tldraw/editor": "workspace:*"
"@tldraw/file-format": "workspace:*"
"@tldraw/tldraw": "workspace:*"
"@tldraw/tlsync-client": "workspace:*"
"@tldraw/ui": "workspace:*"
"@tldraw/utils": "workspace:*"
"@types/fs-extra": ^11.0.1
@ -5149,15 +5124,6 @@ __metadata:
languageName: node
linkType: hard
"@types/react-dom@npm:*, @types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.0.6":
version: 18.0.11
resolution: "@types/react-dom@npm:18.0.11"
dependencies:
"@types/react": "*"
checksum: 579691e4d5ec09688087568037c35edf8cfb1ab3e07f6c60029280733ee7b5c06d66df6fcc90786702c93ac8cb13bc7ff16c79ddfc75d082938fbaa36e1cdbf4
languageName: node
linkType: hard
"@types/react-dom@npm:<18.0.0":
version: 17.0.19
resolution: "@types/react-dom@npm:17.0.19"
@ -5167,6 +5133,15 @@ __metadata:
languageName: node
linkType: hard
"@types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.0.6":
version: 18.0.11
resolution: "@types/react-dom@npm:18.0.11"
dependencies:
"@types/react": "*"
checksum: 579691e4d5ec09688087568037c35edf8cfb1ab3e07f6c60029280733ee7b5c06d66df6fcc90786702c93ac8cb13bc7ff16c79ddfc75d082938fbaa36e1cdbf4
languageName: node
linkType: hard
"@types/react-router-dom@npm:^5.1.8":
version: 5.3.3
resolution: "@types/react-router-dom@npm:5.3.3"
@ -10588,7 +10563,7 @@ __metadata:
languageName: node
linkType: hard
"idb@npm:^7.1.0, idb@npm:^7.1.1":
"idb@npm:^7.1.1":
version: 7.1.1
resolution: "idb@npm:7.1.1"
checksum: 1973c28d53c784b177bdef9f527ec89ec239ec7cf5fcbd987dae75a16c03f5b7dfcc8c6d3285716fd0309dd57739805390bd9f98ce23b1b7d8849a3b52de8d56
@ -11316,6 +11291,16 @@ __metadata:
languageName: node
linkType: hard
"jest-canvas-mock@npm:^2.5.1":
version: 2.5.1
resolution: "jest-canvas-mock@npm:2.5.1"
dependencies:
cssfontparser: ^1.2.1
moo-color: ^1.0.2
checksum: b8ff56c1b7b7feb6d33b7914dbfac21f19a5a33db0bc092f0426e500e80e67df1286bf817eb780e378b648c9130d7b8ca20cd46e45520657996273a948a7c198
languageName: node
linkType: hard
"jest-changed-files@npm:^28.1.3":
version: 28.1.3
resolution: "jest-changed-files@npm:28.1.3"
@ -15392,7 +15377,7 @@ __metadata:
languageName: node
linkType: hard
"react@npm:*, react@npm:18.2.0, react@npm:^18.2.0":
"react@npm:18.2.0, react@npm:^18.2.0":
version: 18.2.0
resolution: "react@npm:18.2.0"
dependencies:
@ -18357,9 +18342,9 @@ __metadata:
linkType: hard
"which-module@npm:^2.0.0":
version: 2.0.1
resolution: "which-module@npm:2.0.1"
checksum: 1967b7ce17a2485544a4fdd9063599f0f773959cca24176dbe8f405e55472d748b7c549cd7920ff6abb8f1ab7db0b0f1b36de1a21c57a8ff741f4f1e792c52be
version: 2.0.0
resolution: "which-module@npm:2.0.0"
checksum: 809f7fd3dfcb2cdbe0180b60d68100c88785084f8f9492b0998c051d7a8efe56784492609d3f09ac161635b78ea29219eb1418a98c15ce87d085bce905705c9c
languageName: node
linkType: hard
@ -18476,7 +18461,7 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^8.10.0, ws@npm:^8.11.0, ws@npm:^8.2.3":
"ws@npm:^8.11.0, ws@npm:^8.2.3":
version: 8.13.0
resolution: "ws@npm:8.13.0"
peerDependencies: