[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
This commit is contained in:
Steve Ruiz 2023-06-01 16:47:34 +01:00 committed by GitHub
parent d6085e4ea6
commit 0c4174c0b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 1430 additions and 1563 deletions

View file

@ -15,4 +15,4 @@ keywords:
Coming soon. 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.

View file

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

View file

@ -33,7 +33,7 @@ export async function cleanup({ page }: PlaywrightTestArgs) {
} }
export async function setupPage(page: PlaywrightTestArgs['page']) { 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.waitForSelector('.tl-canvas')
await page.evaluate(() => (app.enableAnimations = false)) await page.evaluate(() => (app.enableAnimations = false))
} }

View file

@ -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')
})
})

View file

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

View file

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

View file

@ -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
}
>

View file

@ -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'
}

View file

@ -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} />
}
}

View file

@ -1,120 +1,25 @@
import { import { MenuGroup, Tldraw, menuItem, toolbarItem } from '@tldraw/tldraw'
HTMLContainer,
MenuGroup,
menuItem,
TLBaseShape,
TLBoxTool,
TLBoxUtil,
Tldraw,
TldrawEditorConfig,
TLOpacityType,
toolbarItem,
} from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.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 export default function CustomConfigExample() {
// ----------
// 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() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw <Tldraw
persistenceKey="custom-config"
config={customTldrawConfig}
autoFocus autoFocus
tools={tools}
shapes={shapes}
overrides={{ 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) { 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 = { tools.card = {
id: 'card', id: 'card',
icon: 'color', icon: 'color',
@ -127,13 +32,13 @@ export default function Example() {
} }
return tools return tools
}, },
toolbar(app, toolbar, { tools }) { toolbar(_app, toolbar, { tools }) {
// The toolbar is an array of items. We can add it to the // 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. // end of the array or splice it in, then return the array.
toolbar.splice(4, 0, toolbarItem(tools.card)) toolbar.splice(4, 0, toolbarItem(tools.card))
return toolbar return toolbar
}, },
keyboardShortcutsMenu(app, keyboardShortcutsMenu, { tools }) { keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
// Same for the keyboard shortcuts menu, but this menu contains // Same for the keyboard shortcuts menu, but this menu contains
// both items and groups. We want to find the "Tools" group and // both items and groups. We want to find the "Tools" group and
// add it to that before returning the array. // add it to that before returning the array.

View file

@ -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 '@tldraw/tldraw/editor.css'
import { useEffect } from 'react' import { useEffect } from 'react'
import { track } from 'signia-react' import { track } from 'signia-react'
import './custom-ui.css' import './custom-ui.css'
const config = new TldrawEditorConfig() export default function CustomUiExample() {
export default function Example() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<TldrawEditor config={config} autoFocus> <TldrawEditor autoFocus>
<Canvas /> <Canvas />
<CustomUi /> <CustomUi />
</TldrawEditor> </TldrawEditor>

View file

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

View file

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

View file

@ -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,
},
},
})

View file

@ -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>
)
}

View file

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

View file

@ -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!`)
}
}

View file

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

View file

@ -12,7 +12,7 @@ import ExampleBasic from './1-basic/BasicExample'
import CustomComponentsExample from './10-custom-components/CustomComponentsExample' import CustomComponentsExample from './10-custom-components/CustomComponentsExample'
import UserPresenceExample from './11-user-presence/UserPresenceExample' import UserPresenceExample from './11-user-presence/UserPresenceExample'
import UiEventsExample from './12-ui-events/UiEventsExample' 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 PersistenceExample from './14-persistence/PersistenceExample'
import ExampleApi from './2-api/APIExample' import ExampleApi from './2-api/APIExample'
import CustomConfigExample from './3-custom-config/CustomConfigExample' 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 ExplodedExample from './5-exploded/ExplodedExample'
import ExampleScroll from './6-scroll/ScrollExample' import ExampleScroll from './6-scroll/ScrollExample'
import ExampleMultiple from './7-multiple/MultipleExample' 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 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 // 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. // top-level so assets don't need to be passed down in every single example.
@ -34,6 +36,7 @@ type Example = {
path: string path: string
element: JSX.Element element: JSX.Element
} }
export const allExamples: Example[] = [ export const allExamples: Example[] = [
{ {
path: '/', path: '/',
@ -52,7 +55,7 @@ export const allExamples: Example[] = [
element: <ExampleApi />, element: <ExampleApi />,
}, },
{ {
path: '/custom', path: '/custom-config',
element: <CustomConfigExample />, element: <CustomConfigExample />,
}, },
{ {
@ -92,8 +95,8 @@ export const allExamples: Example[] = [
element: <PersistenceExample />, element: <PersistenceExample />,
}, },
{ {
path: '/e2e', path: '/end-to-end',
element: <ForEndToEndTests />, element: <EndToEnd />,
}, },
] ]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -124,7 +124,6 @@
"scripts": { "scripts": {
"dev": "tsx scripts/dev.ts", "dev": "tsx scripts/dev.ts",
"build": "cd ../editor && yarn build && cd ../extension && tsx scripts/build.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", "package": "yarn build && tsx scripts/package.ts",
"publish": "vsce publish", "publish": "vsce publish",
"lint": "yarn run -T tsx ../../../scripts/lint.ts", "lint": "yarn run -T tsx ../../../scripts/lint.ts",

View file

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

View file

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

View file

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

View file

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

View file

@ -1,15 +1,13 @@
import { InstanceRecordType, TLAsset, TLInstanceId, TLStore } from '@tldraw/tlschema' import { TLAsset, TLInstanceId, TLRecord, TLStore } from '@tldraw/tlschema'
import { Store } from '@tldraw/tlstore' import { Store, StoreSnapshot } from '@tldraw/tlstore'
import { annotateError } from '@tldraw/utils' 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 { App } from './app/App'
import { StateNodeConstructor } from './app/statechart/StateNode'
import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls' 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 { DefaultErrorFallback } from './components/DefaultErrorFallback'
import { OptionalErrorBoundary } from './components/ErrorBoundary'
import { ShapeInfo } from './config/createTLStore'
import { AppContext } from './hooks/useApp' import { AppContext } from './hooks/useApp'
import { ContainerProvider, useContainer } from './hooks/useContainer' import { ContainerProvider, useContainer } from './hooks/useContainer'
import { useCursor } from './hooks/useCursor' import { useCursor } from './hooks/useCursor'
@ -21,21 +19,38 @@ import {
} from './hooks/useEditorComponents' } from './hooks/useEditorComponents'
import { useEvent } from './hooks/useEvent' import { useEvent } from './hooks/useEvent'
import { useForceUpdate } from './hooks/useForceUpdate' import { useForceUpdate } from './hooks/useForceUpdate'
import { useLocalStore } from './hooks/useLocalStore'
import { usePreloadAssets } from './hooks/usePreloadAssets' import { usePreloadAssets } from './hooks/usePreloadAssets'
import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix' import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix'
import { useZoomCss } from './hooks/useZoomCss' import { useZoomCss } from './hooks/useZoomCss'
import { StoreWithStatus } from './utils/sync/StoreWithStatus'
import { TAB_ID } from './utils/sync/persistence-constants'
/** @public */ /** @public */
export interface TldrawEditorProps { export type TldrawEditorProps = {
children?: any 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 * @example
* *
@ -49,7 +64,7 @@ export interface TldrawEditorProps {
*/ */
onMount?: (app: App) => void 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. * the canvas.
* *
* @example * @example
@ -81,22 +96,31 @@ export interface TldrawEditorProps {
onCreateBookmarkFromUrl?: ( onCreateBookmarkFromUrl?: (
url: string url: string
) => Promise<{ image: string; title: string; description: 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. * 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 | SyncedStore */
/** store: TLStore | StoreWithStatus
* 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. | {
*/ store?: undefined
instanceId?: TLInstanceId /**
/** Asset URLs */ * The editor's initial data.
assetUrls?: EditorAssetUrls */
/** Whether to automatically focus the editor when it mounts. */ initialData?: StoreSnapshot<TLRecord>
autoFocus?: boolean /**
} * 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 { declare global {
interface Window { interface Window {
@ -105,12 +129,15 @@ declare global {
} }
/** @public */ /** @public */
export function TldrawEditor(props: TldrawEditorProps) { export const TldrawEditor = memo(function TldrawEditor(props: TldrawEditorProps) {
const [container, setContainer] = React.useState<HTMLDivElement | null>(null) const [container, setContainer] = React.useState<HTMLDivElement | null>(null)
const { components, ...rest } = props
const ErrorFallback = const ErrorFallback =
components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback props.components?.ErrorFallback === undefined
? DefaultErrorFallback
: props.components?.ErrorFallback
const { store, ...rest } = props
return ( return (
<div ref={setContainer} draggable={false} className="tl-container tl-theme__light" tabIndex={0}> <div ref={setContainer} draggable={false} className="tl-container tl-theme__light" tabIndex={0}>
@ -120,51 +147,68 @@ export function TldrawEditor(props: TldrawEditorProps) {
> >
{container && ( {container && (
<ContainerProvider container={container}> <ContainerProvider container={container}>
<EditorComponentsProvider overrides={components}> <EditorComponentsProvider overrides={props.components}>
<TldrawEditorBeforeLoading {...rest} /> {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> </EditorComponentsProvider>
</ContainerProvider> </ContainerProvider>
)} )}
</OptionalErrorBoundary> </OptionalErrorBoundary>
</div> </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( const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(
props.assetUrls ?? defaultEditorAssetUrls assetUrls ?? defaultEditorAssetUrls
) )
const _store = useMemo<TLStore | SyncedStore>(() => { switch (store.status) {
return ( case 'error': {
store ??
config.createStore({
instanceId: instanceId ?? InstanceRecordType.createId(),
})
)
}, [store, config, instanceId])
let loadedStore: TLStore | SyncedStore
if (!(_store instanceof Store)) {
if (_store.error) {
// for error handling, we fall back to the default error boundary. // for error handling, we fall back to the default error boundary.
// if users want to handle this error differently, they can render // if users want to handle this error differently, they can render
// their own error screen before the TldrawEditor component // their own error screen before the TldrawEditor component
throw _store.error throw store.error
} }
if (!_store.store) { case 'loading': {
return <LoadingScreen>Connecting...</LoadingScreen> return <LoadingScreen>Connecting...</LoadingScreen>
} }
case 'not-synced': {
loadedStore = _store.store break
} else { }
loadedStore = _store case 'synced-local': {
} break
}
if (instanceId && loadedStore.props.instanceId !== instanceId) { case 'synced-remote': {
console.error( break
`The store's instanceId (${loadedStore.props.instanceId}) does not match the instanceId prop (${instanceId}). This may cause unexpected behavior.` }
)
} }
if (preloadingError) { if (preloadingError) {
@ -175,57 +219,56 @@ function TldrawEditorBeforeLoading({ config, instanceId, store, ...props }: Tldr
return <LoadingScreen>Loading assets...</LoadingScreen> return <LoadingScreen>Loading assets...</LoadingScreen>
} }
return <TldrawEditorAfterLoading {...props} store={loadedStore} config={config} /> return <TldrawEditorWithReadyStore {...rest} store={store.store} />
} })
function TldrawEditorAfterLoading({ function TldrawEditorWithReadyStore({
onMount, onMount,
config,
children, children,
onCreateAssetFromFile, onCreateAssetFromFile,
onCreateBookmarkFromUrl, onCreateBookmarkFromUrl,
store, store,
tools,
shapes,
autoFocus, autoFocus,
}: Omit<TldrawEditorProps, 'store' | 'config' | 'instanceId' | 'userId'> & { }: TldrawEditorProps & {
config: TldrawEditorConfig
store: TLStore store: TLStore
}) { }) {
const container = useContainer()
const [app, setApp] = React.useState<App | null>(null)
const { ErrorFallback } = useEditorComponents() const { ErrorFallback } = useEditorComponents()
const container = useContainer()
const [app, setApp] = useState<App | null>(null)
React.useLayoutEffect(() => { useLayoutEffect(() => {
const app = new App({ const app = new App({
store, store,
config, shapes,
tools,
getContainer: () => container, getContainer: () => container,
}) })
setApp(app)
if (autoFocus) {
app.focus()
}
;(window as any).app = app ;(window as any).app = app
setApp(app)
return () => { return () => {
app.dispose() app.dispose()
setApp((prevApp) => (prevApp === app ? null : prevApp))
} }
}, [container, config, store, autoFocus]) }, [container, shapes, tools, store])
React.useEffect(() => { React.useEffect(() => {
if (app) { if (!app) return
// Overwrite the default onCreateAssetFromFile handler.
if (onCreateAssetFromFile) {
app.onCreateAssetFromFile = onCreateAssetFromFile
}
if (onCreateBookmarkFromUrl) { // Overwrite the default onCreateAssetFromFile handler.
app.onCreateBookmarkFromUrl = onCreateBookmarkFromUrl if (onCreateAssetFromFile) {
} app.onCreateAssetFromFile = onCreateAssetFromFile
}
if (onCreateBookmarkFromUrl) {
app.onCreateBookmarkFromUrl = onCreateBookmarkFromUrl
} }
}, [app, onCreateAssetFromFile, onCreateBookmarkFromUrl]) }, [app, onCreateAssetFromFile, onCreateBookmarkFromUrl])
React.useLayoutEffect(() => {
if (app && autoFocus) app.focus()
}, [app, autoFocus])
const onMountEvent = useEvent((app: App) => { const onMountEvent = useEvent((app: App) => {
onMount?.(app) onMount?.(app)
app.emit('mount') app.emit('mount')
@ -233,10 +276,7 @@ function TldrawEditorAfterLoading({
}) })
React.useEffect(() => { React.useEffect(() => {
if (app) { if (app) onMountEvent(app)
// Run onMount
onMountEvent(app)
}
}, [app, onMountEvent]) }, [app, onMountEvent])
const crashingError = useSyncExternalStore( const crashingError = useSyncExternalStore(

View file

@ -64,7 +64,7 @@ import {
isShape, isShape,
isShapeId, isShapeId,
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
import { ComputedCache, HistoryEntry, UnknownRecord } from '@tldraw/tlstore' import { ComputedCache, HistoryEntry, RecordType, UnknownRecord } from '@tldraw/tlstore'
import { import {
annotateError, annotateError,
compact, compact,
@ -77,7 +77,10 @@ import {
import { EventEmitter } from 'eventemitter3' import { EventEmitter } from 'eventemitter3'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { EMPTY_ARRAY, atom, computed, transact } from 'signia' 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 { import {
ANIMATION_MEDIUM_MS, ANIMATION_MEDIUM_MS,
BLACKLISTED_PROPS, BLACKLISTED_PROPS,
@ -132,7 +135,7 @@ import { TLResizeMode, TLShapeUtil } from './shapeutils/TLShapeUtil'
import { TLTextUtil } from './shapeutils/TLTextUtil/TLTextUtil' import { TLTextUtil } from './shapeutils/TLTextUtil/TLTextUtil'
import { TLExportColors } from './shapeutils/shared/TLExportColors' import { TLExportColors } from './shapeutils/shared/TLExportColors'
import { RootState } from './statechart/RootState' import { RootState } from './statechart/RootState'
import { StateNode } from './statechart/StateNode' import { StateNode, StateNodeConstructor } from './statechart/StateNode'
import { TLClipboardModel } from './types/clipboard-types' import { TLClipboardModel } from './types/clipboard-types'
import { TLEventMap } from './types/emit-types' import { TLEventMap } from './types/emit-types'
import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo } from './types/event-types' import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo } from './types/event-types'
@ -161,8 +164,18 @@ export interface AppOptions {
* from a server or database. * from a server or database.
*/ */
store: TLStore 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 * Should return a containing html element which has all the styles applied to the app. If not
* given, the body element will be used. * given, the body element will be used.
@ -177,28 +190,54 @@ export function isShapeWithHandles(shape: TLShape) {
/** @public */ /** @public */
export class App extends EventEmitter<TLEventMap> { export class App extends EventEmitter<TLEventMap> {
constructor({ config, store, getContainer }: AppOptions) { constructor({
store,
user,
tools = defaultTools,
shapes = defaultShapes,
getContainer,
}: AppOptions) {
super() 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.store = store
this.user = new UserPreferencesManager(this) this.user = new UserPreferencesManager(user ?? createTLUser())
this.getContainer = getContainer ?? (() => document.body) this.getContainer = getContainer ?? (() => document.body)
this.textMeasure = new TextManager(this) this.textMeasure = new TextManager(this)
// Set the shape utils this.root = new RootState(this)
this.shapeUtils = Object.fromEntries(
Object.entries(this.config.shapeUtils).map(([type, Util]) => [type, new Util(this, type)]) // 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) { if (typeof window !== 'undefined' && 'navigator' in window) {
this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
this.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i) this.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i)
@ -212,13 +251,6 @@ export class App extends EventEmitter<TLEventMap> {
// Set styles // Set styles
this.colors = new Map(App.styles.color.map((c) => [c.id, `var(--palette-${c.id})`])) 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) => { this.store.onBeforeDelete = (record) => {
if (record.typeName === 'shape') { if (record.typeName === 'shape') {
this._shapeWillBeDeleted(record) this._shapeWillBeDeleted(record)
@ -310,13 +342,6 @@ export class App extends EventEmitter<TLEventMap> {
*/ */
readonly store: TLStore readonly store: TLStore
/**
* The editor's config
*
* @public
*/
readonly config: TldrawEditorConfig
/** /**
* The root state of the statechart. * 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 // When we create the shape, take in the partial (the props coming into the
// function) and merge it with the default props. // 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, ...partial,
index, index,
parentId: partial.parentId ?? focusLayerId, parentId: partial.parentId ?? focusLayerId,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -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,
},
})
}
}

View file

@ -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,
},
})
}

View file

@ -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,
}
}

View file

@ -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,
},
}

View file

@ -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,
]

View file

@ -4,12 +4,14 @@ import { useApp } from './useApp'
export function useCoarsePointer() { export function useCoarsePointer() {
const app = useApp() const app = useApp()
useEffect(() => { useEffect(() => {
const mql = window.matchMedia('(pointer: coarse)') if (window.matchMedia) {
const handler = () => { const mql = window.matchMedia('(pointer: coarse)')
app.isCoarsePointer = mql.matches 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]) }, [app])
} }

View file

@ -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' }
}

View file

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

View file

@ -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
}

View file

@ -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
}

View file

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

View file

@ -1,7 +1,12 @@
import { render, screen } from '@testing-library/react' import { act, render, screen } from '@testing-library/react'
import { InstanceRecordType } from '@tldraw/tlschema' import { InstanceRecordType, TLBaseShape, TLOpacityType } from '@tldraw/tlschema'
import { TldrawEditor } from '../TldrawEditor' 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 let originalFetch: typeof window.fetch
beforeEach(() => { beforeEach(() => {
@ -9,7 +14,6 @@ beforeEach(() => {
if (args[0] === '/icons/icon/icon-names.json') { if (args[0] === '/icons/icon/icon-names.json') {
return Promise.resolve({ json: () => Promise.resolve([]) } as Response) return Promise.resolve({ json: () => Promise.resolve([]) } as Response)
} }
return originalFetch(...args) return originalFetch(...args)
}) })
}) })
@ -19,43 +23,75 @@ afterEach(() => {
window.fetch = originalFetch window.fetch = originalFetch
}) })
describe('<Tldraw />', () => { describe('<TldrawEditor />', () => {
it('Accepts fresh versions of store and calls `onMount` for each one', async () => { it('Renders without crashing', async () => {
const config = new TldrawEditorConfig() 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'), instanceId: InstanceRecordType.createCustomId('test'),
}) })
const onMount = jest.fn() const onMount = jest.fn()
const rendered = render( const rendered = render(
<TldrawEditor config={config} store={initialStore} onMount={onMount} autoFocus> <TldrawEditor store={initialStore} onMount={onMount} autoFocus>
<div data-testid="canvas-1" /> <div data-testid="canvas-1" />
</TldrawEditor> </TldrawEditor>
) )
await screen.findByTestId('canvas-1') await screen.findByTestId('canvas-1')
expect(onMount).toHaveBeenCalledTimes(1)
const initialApp = onMount.mock.lastCall[0] const initialApp = onMount.mock.lastCall[0]
jest.spyOn(initialApp, 'dispose') jest.spyOn(initialApp, 'dispose')
expect(initialApp.store).toBe(initialStore) expect(initialApp.store).toBe(initialStore)
// re-render with the same store: // re-render with the same store:
rendered.rerender( rendered.rerender(
<TldrawEditor config={config} store={initialStore} onMount={onMount} autoFocus> <TldrawEditor store={initialStore} onMount={onMount} autoFocus>
<div data-testid="canvas-2" /> <div data-testid="canvas-2" />
</TldrawEditor> </TldrawEditor>
) )
await screen.findByTestId('canvas-2') await screen.findByTestId('canvas-2')
// not called again: // not called again:
expect(onMount).toHaveBeenCalledTimes(1) expect(onMount).toHaveBeenCalledTimes(1)
// re-render with a new store: // re-render with a new store:
const newStore = config.createStore({ const newStore = createTLStore({
instanceId: InstanceRecordType.createCustomId('test'), instanceId: InstanceRecordType.createCustomId('test'),
}) })
rendered.rerender( rendered.rerender(
<TldrawEditor config={config} store={newStore} onMount={onMount} autoFocus> <TldrawEditor store={newStore} onMount={onMount} autoFocus>
<div data-testid="canvas-3" /> <div data-testid="canvas-3" />
</TldrawEditor> </TldrawEditor>
) )
@ -64,4 +100,188 @@ describe('<Tldraw />', () => {
expect(onMount).toHaveBeenCalledTimes(2) expect(onMount).toHaveBeenCalledTimes(2)
expect(onMount.mock.lastCall[0].store).toBe(newStore) 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')
})
}) })

View file

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

View file

@ -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
}

View file

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

View file

@ -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 { RecordsDiff, SerializedSchema, compareSchemas, squashRecordDiffs } from '@tldraw/tlstore'
import { assert, hasOwnProperty } from '@tldraw/utils' import { assert, hasOwnProperty } from '@tldraw/utils'
import { transact } from 'signia' import { transact } from 'signia'

View file

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

View file

@ -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 const tabIdKey = 'TLDRAW_TAB_ID_v2' as const

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,5 @@ import '@tldraw/polyfills'
// eslint-disable-next-line local/no-export-star // eslint-disable-next-line local/no-export-star
export * from '@tldraw/editor' export * from '@tldraw/editor'
// eslint-disable-next-line local/no-export-star // 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 * from '@tldraw/ui'
export { Tldraw } from './lib/Tldraw' export { Tldraw } from './lib/Tldraw'

View file

@ -1,38 +1,12 @@
import { Canvas, TldrawEditor, TldrawEditorConfig, TldrawEditorProps } from '@tldraw/editor' import { Canvas, TldrawEditor, TldrawEditorProps } from '@tldraw/editor'
import { DEFAULT_DOCUMENT_NAME, TAB_ID, useLocalSyncClient } from '@tldraw/tlsync-client' import { ContextMenu, TldrawUi, TldrawUiProps } from '@tldraw/ui'
import { ContextMenu, TldrawUi, TldrawUiContextProviderProps } from '@tldraw/ui'
import { useMemo } from 'react'
/** @public */ /** @public */
export function Tldraw( export function Tldraw(props: TldrawEditorProps & TldrawUiProps) {
props: Omit<TldrawEditorProps, 'store' | 'config'> & const { children, ...rest } = props
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,
})
return ( return (
<TldrawEditor {...rest} instanceId={instanceId} store={syncedStore} config={_config}> <TldrawEditor {...rest}>
<TldrawUi {...rest}> <TldrawUi {...rest}>
<ContextMenu> <ContextMenu>
<Canvas /> <Canvas />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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.

View file

@ -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).

View file

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

View file

@ -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)
```

View file

@ -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"
}
}

View file

@ -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
},
}

View file

@ -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'

View file

@ -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' }
}

View file

@ -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" }]
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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