[refactor] restore createTLSchema (#1444)

This PR restores `createTLSchema`. 

It also:
- removes `TldrawEditorConfig.default`
- makes `config` a required property of `<TldrawEditor>`, though it's
created automatically in `<Tldraw>`.
- makes `config` a required property of `App`
- removes `TLShapeType` and replaces the rare usage with
`TLShape["type"]`
- adds `TLDefaultShape` for a union of our default shapes
- makes `TLShape` a union of `TLDefaultShape` and `TLUnknownShape`

### Change Type

- [x] `major` — Breaking Change

### Release Notes

- [editor] Simplifies custom shape definition
- [tldraw] Updates props for <TldrawEditor> component to require a
`TldrawEditorConfig`.
This commit is contained in:
Steve Ruiz 2023-05-24 11:48:31 +01:00 committed by GitHub
parent f3182c9874
commit eb26964130
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 377 additions and 285 deletions

View file

@ -1,13 +1,15 @@
import { Canvas, TldrawEditor, useApp } from '@tldraw/tldraw' import { Canvas, TldrawEditor, TldrawEditorConfig, 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 Example() { export default function Example() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<TldrawEditor autoFocus> <TldrawEditor config={config} autoFocus>
<Canvas /> <Canvas />
<CustomUi /> <CustomUi />
</TldrawEditor> </TldrawEditor>

View file

@ -3,6 +3,7 @@ import {
ContextMenu, ContextMenu,
getUserData, getUserData,
TldrawEditor, TldrawEditor,
TldrawEditorConfig,
TldrawUi, TldrawUi,
TLInstance, TLInstance,
useLocalSyncClient, useLocalSyncClient,
@ -12,10 +13,13 @@ import '@tldraw/tldraw/ui.css'
const instanceId = TLInstance.createCustomId('example') const instanceId = TLInstance.createCustomId('example')
const config = new TldrawEditorConfig()
export default function Example() { export default function Example() {
const userData = getUserData() const userData = getUserData()
const syncedStore = useLocalSyncClient({ const syncedStore = useLocalSyncClient({
config,
instanceId, instanceId,
userId: userData.id, userId: userData.id,
universalPersistenceKey: 'exploded-example', universalPersistenceKey: 'exploded-example',
@ -24,7 +28,13 @@ export default function Example() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<TldrawEditor instanceId={instanceId} userId={userData.id} store={syncedStore} autoFocus> <TldrawEditor
instanceId={instanceId}
userId={userData.id}
store={syncedStore}
config={config}
autoFocus
>
<TldrawUi> <TldrawUi>
<ContextMenu> <ContextMenu>
<Canvas /> <Canvas />

View file

@ -4,6 +4,7 @@ import {
ErrorBoundary, ErrorBoundary,
setRuntimeOverrides, setRuntimeOverrides,
TldrawEditor, TldrawEditor,
TldrawEditorConfig,
TLUserId, TLUserId,
} from '@tldraw/editor' } from '@tldraw/editor'
import { linksUiOverrides } from './utils/links' import { linksUiOverrides } from './utils/links'
@ -24,6 +25,8 @@ 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 // @ts-ignore
setRuntimeOverrides({ setRuntimeOverrides({
@ -96,6 +99,7 @@ export const TldrawWrapper = () => {
uri: message.data.uri, uri: message.data.uri,
userId: message.data.userId as TLUserId, userId: message.data.userId as TLUserId,
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)
@ -126,20 +130,30 @@ export type TLDrawInnerProps = {
uri: string uri: string
userId: TLUserId userId: TLUserId
isDarkMode: boolean isDarkMode: boolean
config: TldrawEditorConfig
} }
function TldrawInner({ uri, assetSrc, userId, isDarkMode, fileContents }: TLDrawInnerProps) { function TldrawInner({
uri,
config,
assetSrc,
userId,
isDarkMode,
fileContents,
}: TLDrawInnerProps) {
const instanceId = TAB_ID const instanceId = TAB_ID
const syncedStore = useLocalSyncClient({ const syncedStore = useLocalSyncClient({
universalPersistenceKey: uri, universalPersistenceKey: uri,
instanceId, instanceId,
userId, userId,
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}
userId={userId} userId={userId}

View file

@ -4,13 +4,13 @@ import * as vscode from 'vscode'
export const defaultFileContents: TldrawFile = { export const defaultFileContents: TldrawFile = {
tldrawFileFormatVersion: 1, tldrawFileFormatVersion: 1,
schema: TldrawEditorConfig.default.storeSchema.serialize(), schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [], records: [],
} }
export const fileContentWithErrors: TldrawFile = { export const fileContentWithErrors: TldrawFile = {
tldrawFileFormatVersion: 1, tldrawFileFormatVersion: 1,
schema: TldrawEditorConfig.default.storeSchema.serialize(), schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [{ typeName: 'shape', id: null } as any], records: [{ typeName: 'shape', id: null } as any],
} }

View file

@ -12,11 +12,16 @@ export async function pointWithinActiveArea(x: number, y: number) {
} }
export async function waitForReady() { export async function waitForReady() {
await browser.waitUntil(() => { await Promise.any([
return browser.execute(() => { new Promise<boolean>((r) => {
return window.tldrawReady browser.waitUntil(() => browser.execute(() => window.tldrawReady)).then(() => r(true))
}) }),
}) new Promise<boolean>((r) => {
// eslint-disable-next-line no-console
console.log('waitFor failed, using timeout')
setTimeout(() => r(true), 2000)
}),
])
// Make sure the window is focused... maybe // Make sure the window is focused... maybe
await ui.canvas.click(100, 100) await ui.canvas.click(100, 100)

View file

@ -44,7 +44,6 @@ import { sortByIndex } from '@tldraw/indices';
import { StoreSchema } from '@tldraw/tlstore'; 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 { T } from '@tldraw/tlvalidate';
import { TLAlignType } from '@tldraw/tlschema'; import { TLAlignType } from '@tldraw/tlschema';
import { TLArrowheadType } from '@tldraw/tlschema'; import { TLArrowheadType } from '@tldraw/tlschema';
import { TLArrowShape } from '@tldraw/tlschema'; import { TLArrowShape } from '@tldraw/tlschema';
@ -87,7 +86,6 @@ import { TLShapeId } from '@tldraw/tlschema';
import { TLShapePartial } from '@tldraw/tlschema'; import { TLShapePartial } from '@tldraw/tlschema';
import { TLShapeProp } from '@tldraw/tlschema'; import { TLShapeProp } from '@tldraw/tlschema';
import { TLShapeProps } from '@tldraw/tlschema'; import { TLShapeProps } from '@tldraw/tlschema';
import { TLShapeType } 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';
@ -274,7 +272,7 @@ export class App extends EventEmitter<TLEventMap> {
getPageTransform(shape: TLShape): Matrix2d | undefined; getPageTransform(shape: TLShape): Matrix2d | undefined;
getPageTransformById(id: TLShapeId): Matrix2d | undefined; getPageTransformById(id: TLShapeId): Matrix2d | undefined;
// (undocumented) // (undocumented)
getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShapeType): TLPageId | TLShapeId; getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShape['type']): TLPageId | TLShapeId;
getParentPageId(shape?: TLShape): TLPageId | undefined; getParentPageId(shape?: TLShape): TLPageId | undefined;
getParentShape(shape?: TLShape): TLShape | undefined; getParentShape(shape?: TLShape): TLShape | undefined;
getParentsMappedToChildren(ids: TLShapeId[]): Map<TLParentId, Set<TLShape>>; getParentsMappedToChildren(ids: TLShapeId[]): Map<TLParentId, Set<TLShape>>;
@ -556,7 +554,7 @@ export function applyRotationToSnapshotShapes({ delta, app, snapshot, stage, }:
// @public (undocumented) // @public (undocumented)
export interface AppOptions { export interface AppOptions {
config?: TldrawEditorConfig; config: TldrawEditorConfig;
getContainer: () => HTMLElement; getContainer: () => HTMLElement;
store: TLStore; store: TLStore;
} }
@ -1793,7 +1791,7 @@ export function TldrawEditor(props: TldrawEditorProps): JSX.Element;
// @public (undocumented) // @public (undocumented)
export class TldrawEditorConfig { export class TldrawEditorConfig {
constructor(opts: TldrawEditorConfigOptions); constructor(opts?: TldrawEditorConfigOptions);
// (undocumented) // (undocumented)
createStore(config: { createStore(config: {
initialData?: StoreSnapshot<TLRecord>; initialData?: StoreSnapshot<TLRecord>;
@ -1801,13 +1799,7 @@ export class TldrawEditorConfig {
instanceId: TLInstanceId; instanceId: TLInstanceId;
}): TLStore; }): TLStore;
// (undocumented) // (undocumented)
static readonly default: TldrawEditorConfig; readonly shapeUtils: Record<TLShape['type'], TLShapeUtilConstructor<any>>;
// (undocumented)
readonly shapeMigrations: MigrationsForShapes<TLShape>;
// (undocumented)
readonly shapeUtils: UtilsForShapes<TLShape>;
// (undocumented)
readonly shapeValidators: Record<TLShape['type'], T.Validator<any>>;
// (undocumented) // (undocumented)
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>; readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>;
// (undocumented) // (undocumented)
@ -1823,7 +1815,7 @@ export interface TldrawEditorProps {
// (undocumented) // (undocumented)
children?: any; children?: any;
components?: Partial<TLEditorComponents>; components?: Partial<TLEditorComponents>;
config?: TldrawEditorConfig; config: TldrawEditorConfig;
instanceId?: TLInstanceId; instanceId?: TLInstanceId;
isDarkMode?: boolean; isDarkMode?: boolean;
onCreateAssetFromFile?: (file: File) => Promise<TLAsset>; onCreateAssetFromFile?: (file: File) => Promise<TLAsset>;
@ -2044,7 +2036,7 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
// (undocumented) // (undocumented)
canEdit: () => boolean; canEdit: () => boolean;
// (undocumented) // (undocumented)
canReceiveNewChildrenOfType: (_type: TLShapeType) => boolean; canReceiveNewChildrenOfType: (_type: TLShape['type']) => boolean;
// (undocumented) // (undocumented)
defaultProps(): TLFrameShape['props']; defaultProps(): TLFrameShape['props'];
// (undocumented) // (undocumented)
@ -2456,7 +2448,7 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
canCrop: TLShapeUtilFlag<T>; canCrop: TLShapeUtilFlag<T>;
canDropShapes(shape: T, shapes: TLShape[]): boolean; canDropShapes(shape: T, shapes: TLShape[]): boolean;
canEdit: TLShapeUtilFlag<T>; canEdit: TLShapeUtilFlag<T>;
canReceiveNewChildrenOfType(type: TLShapeType): boolean; canReceiveNewChildrenOfType(type: TLShape['type']): boolean;
canResize: TLShapeUtilFlag<T>; canResize: TLShapeUtilFlag<T>;
canScroll: TLShapeUtilFlag<T>; canScroll: TLShapeUtilFlag<T>;
canUnmount: TLShapeUtilFlag<T>; canUnmount: TLShapeUtilFlag<T>;

View file

@ -1,7 +1,7 @@
import { TLAsset, TLInstance, TLInstanceId, TLStore, TLUser, TLUserId } from '@tldraw/tlschema' import { TLAsset, TLInstance, TLInstanceId, TLStore, TLUser, TLUserId } from '@tldraw/tlschema'
import { Store } from '@tldraw/tlstore' import { Store } from '@tldraw/tlstore'
import { annotateError } from '@tldraw/utils' import { annotateError } from '@tldraw/utils'
import React, { useCallback, useSyncExternalStore } from 'react' import React, { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
import { App } from './app/App' import { App } from './app/App'
import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls' import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls'
import { OptionalErrorBoundary } from './components/ErrorBoundary' import { OptionalErrorBoundary } from './components/ErrorBoundary'
@ -28,12 +28,12 @@ import { useZoomCss } from './hooks/useZoomCss'
/** @public */ /** @public */
export interface TldrawEditorProps { export interface 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 */ /** Overrides for the tldraw components */
components?: Partial<TLEditorComponents> components?: Partial<TLEditorComponents>
/** Whether to display the dark mode. */ /** Whether to display the dark mode. */
isDarkMode?: boolean isDarkMode?: boolean
/** A configuration defining major customizations to the app, such as custom shapes and new tools */
config?: TldrawEditorConfig
/** /**
* Called when the app has mounted. * Called when the app has mounted.
* *
@ -133,7 +133,7 @@ export function TldrawEditor(props: TldrawEditorProps) {
} }
function TldrawEditorBeforeLoading({ function TldrawEditorBeforeLoading({
config = TldrawEditorConfig.default, config,
userId, userId,
instanceId, instanceId,
store, store,
@ -143,26 +143,43 @@ function TldrawEditorBeforeLoading({
props.assetUrls ?? defaultEditorAssetUrls props.assetUrls ?? defaultEditorAssetUrls
) )
store ??= config.createStore({ const [_store, _setStore] = useState<TLStore | SyncedStore>(() => {
userId: userId ?? TLUser.createId(), return (
instanceId: instanceId ?? TLInstance.createId(), store ??
config.createStore({
userId: userId ?? TLUser.createId(),
instanceId: instanceId ?? TLInstance.createId(),
})
)
}) })
let loadedStore useEffect(() => {
if (!(store instanceof Store)) { _setStore(() => {
if (store.error) { return (
store ??
config.createStore({
userId: userId ?? TLUser.createId(),
instanceId: instanceId ?? TLInstance.createId(),
})
)
})
}, [store, config, userId, 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) { if (!_store.store) {
return <LoadingScreen>Connecting...</LoadingScreen> return <LoadingScreen>Connecting...</LoadingScreen>
} }
loadedStore = store.store loadedStore = _store.store
} else { } else {
loadedStore = store loadedStore = _store
} }
if (instanceId && loadedStore.props.instanceId !== instanceId) { if (instanceId && loadedStore.props.instanceId !== instanceId) {
@ -209,8 +226,8 @@ function TldrawEditorAfterLoading({
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
const app = new App({ const app = new App({
store, store,
getContainer: () => container,
config, config,
getContainer: () => container,
}) })
setApp(app) setApp(app)

View file

@ -49,7 +49,6 @@ import {
TLShapeId, TLShapeId,
TLShapePartial, TLShapePartial,
TLShapeProp, TLShapeProp,
TLShapeType,
TLSizeStyle, TLSizeStyle,
TLStore, TLStore,
TLUnknownShape, TLUnknownShape,
@ -160,7 +159,7 @@ export interface AppOptions {
*/ */
store: TLStore store: TLStore
/** A configuration defining major customizations to the app, such as custom shapes and new tools */ /** A configuration defining major customizations to the app, such as custom shapes and new tools */
config?: TldrawEditorConfig config: TldrawEditorConfig
/** /**
* 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.
@ -175,14 +174,15 @@ export function isShapeWithHandles(shape: TLShape) {
/** @public */ /** @public */
export class App extends EventEmitter<TLEventMap> { export class App extends EventEmitter<TLEventMap> {
constructor({ config = TldrawEditorConfig.default, store, getContainer }: AppOptions) { constructor({ config, store, getContainer }: AppOptions) {
super() super()
if (store.schema !== config.storeSchema) { this.config = config
if (store.schema !== this.config.storeSchema) {
throw new Error('Store schema does not match schema given to App') throw new Error('Store schema does not match schema given to App')
} }
this.config = config
this.store = store this.store = store
this.getContainer = getContainer ?? (() => document.body) this.getContainer = getContainer ?? (() => document.body)
@ -191,7 +191,7 @@ export class App extends EventEmitter<TLEventMap> {
// Set the shape utils // Set the shape utils
this.shapeUtils = Object.fromEntries( this.shapeUtils = Object.fromEntries(
Object.entries(config.shapeUtils).map(([type, Util]) => [type, new Util(this, type)]) Object.entries(this.config.shapeUtils).map(([type, Util]) => [type, new Util(this, type)])
) )
if (typeof window !== 'undefined' && 'navigator' in window) { if (typeof window !== 'undefined' && 'navigator' in window) {
@ -209,7 +209,7 @@ export class App extends EventEmitter<TLEventMap> {
this.root = new RootState(this) this.root = new RootState(this)
if (this.root.children) { if (this.root.children) {
config.tools.forEach((Ctor) => { this.config.tools.forEach((Ctor) => {
this.root.children![Ctor.id] = new Ctor(this) this.root.children![Ctor.id] = new Ctor(this)
}) })
} }
@ -2864,7 +2864,7 @@ export class App extends EventEmitter<TLEventMap> {
return this return this
} }
getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShapeType) { getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShape['type']) {
const shapes = this.sortedShapesArray const shapes = this.sortedShapesArray
for (let i = shapes.length - 1; i >= 0; i--) { for (let i = shapes.length - 1; i >= 0; i--) {

View file

@ -1,5 +1,5 @@
import { canolicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives' import { canolicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives'
import { TLFrameShape, TLShape, TLShapeId, TLShapeType } from '@tldraw/tlschema' import { TLFrameShape, TLShape, TLShapeId } from '@tldraw/tlschema'
import { last } from '@tldraw/utils' import { last } from '@tldraw/utils'
import { SVGContainer } from '../../../components/SVGContainer' import { SVGContainer } from '../../../components/SVGContainer'
import { defaultEmptyAs } from '../../../utils/string' import { defaultEmptyAs } from '../../../utils/string'
@ -148,7 +148,7 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
) )
} }
override canReceiveNewChildrenOfType = (_type: TLShapeType) => { override canReceiveNewChildrenOfType = (_type: TLShape['type']) => {
return true return true
} }

View file

@ -5,7 +5,6 @@ import {
TLHandle, TLHandle,
TLShape, TLShape,
TLShapePartial, TLShapePartial,
TLShapeType,
TLUnknownShape, TLUnknownShape,
Vec2dModel, Vec2dModel,
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
@ -308,7 +307,7 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param type - The shape type. * @param type - The shape type.
* @public * @public
*/ */
canReceiveNewChildrenOfType(type: TLShapeType) { canReceiveNewChildrenOfType(type: TLShape['type']) {
return false return false
} }

View file

@ -1,4 +1,4 @@
import { TLShapeType, TLStyleType } from '@tldraw/tlschema' import { TLStyleType } from '@tldraw/tlschema'
import { StateNode } from '../StateNode' import { StateNode } from '../StateNode'
import { Idle } from './children/Idle' import { Idle } from './children/Idle'
import { Pointing } from './children/Pointing' import { Pointing } from './children/Pointing'
@ -8,7 +8,7 @@ export class TLArrowTool extends StateNode {
static initial = 'idle' static initial = 'idle'
static children = () => [Idle, Pointing] static children = () => [Idle, Pointing]
shapeType: TLShapeType = 'arrow' shapeType = 'arrow'
styles = [ styles = [
'color', 'color',

View file

@ -1,4 +1,4 @@
import { createShapeId, TLArrowShape, TLShapeType } from '@tldraw/tlschema' import { createShapeId, TLArrowShape } from '@tldraw/tlschema'
import { TLArrowUtil } from '../../../shapeutils/TLArrowUtil/TLArrowUtil' import { TLArrowUtil } from '../../../shapeutils/TLArrowUtil/TLArrowUtil'
import { TLEventHandlers } from '../../../types/event-types' import { TLEventHandlers } from '../../../types/event-types'
import { StateNode } from '../../StateNode' import { StateNode } from '../../StateNode'
@ -7,8 +7,6 @@ import { TLArrowTool } from '../TLArrowTool'
export class Pointing extends StateNode { export class Pointing extends StateNode {
static override id = 'pointing' static override id = 'pointing'
shapeType = '' as TLShapeType
shape?: TLArrowShape shape?: TLArrowShape
preciseTimeout = -1 preciseTimeout = -1
@ -33,7 +31,7 @@ export class Pointing extends StateNode {
this.didTimeout = false this.didTimeout = false
this.shapeType = (this.parent as TLArrowTool).shapeType const shapeType = (this.parent as TLArrowTool).shapeType
this.app.mark('creating') this.app.mark('creating')
@ -42,7 +40,7 @@ export class Pointing extends StateNode {
this.app.createShapes([ this.app.createShapes([
{ {
id, id,
type: this.shapeType, type: shapeType,
x: currentPagePoint.x, x: currentPagePoint.x,
y: currentPagePoint.y, y: currentPagePoint.y,
}, },

View file

@ -1,4 +1,4 @@
import { TLShapeType, TLStyleType } from '@tldraw/tlschema' import { TLStyleType } from '@tldraw/tlschema'
import { StateNode } from '../StateNode' import { StateNode } from '../StateNode'
import { Idle } from './children/Idle' import { Idle } from './children/Idle'
@ -9,7 +9,7 @@ export class TLLineTool extends StateNode {
static initial = 'idle' static initial = 'idle'
static children = () => [Idle, Pointing] static children = () => [Idle, Pointing]
shapeType: TLShapeType = 'line' shapeType = 'line'
styles = ['color', 'opacity', 'dash', 'size', 'spline'] as TLStyleType[] styles = ['color', 'opacity', 'dash', 'size', 'spline'] as TLStyleType[]
} }

View file

@ -1,6 +1,6 @@
import { getIndexAbove, sortByIndex } from '@tldraw/indices' import { getIndexAbove, sortByIndex } from '@tldraw/indices'
import { Matrix2d, Vec2d } from '@tldraw/primitives' import { Matrix2d, Vec2d } from '@tldraw/primitives'
import { TLHandle, TLLineShape, TLShapeId, TLShapeType, createShapeId } from '@tldraw/tlschema' import { TLHandle, TLLineShape, TLShapeId, createShapeId } from '@tldraw/tlschema'
import { last, structuredClone } from '@tldraw/utils' import { last, structuredClone } from '@tldraw/utils'
import { TLEventHandlers, TLInterruptEvent } from '../../../types/event-types' import { TLEventHandlers, TLInterruptEvent } from '../../../types/event-types'
import { StateNode } from '../../StateNode' import { StateNode } from '../../StateNode'
@ -9,8 +9,6 @@ import { TLLineTool } from '../TLLineTool'
export class Pointing extends StateNode { export class Pointing extends StateNode {
static override id = 'pointing' static override id = 'pointing'
shapeType = '' as TLShapeType
shape = {} as TLLineShape shape = {} as TLLineShape
markPointId = '' markPointId = ''
@ -19,7 +17,6 @@ export class Pointing extends StateNode {
const { inputs } = this.app const { inputs } = this.app
const { currentPagePoint } = inputs const { currentPagePoint } = inputs
this.shapeType = (this.parent as TLLineTool).shapeType
this.markPointId = this.app.mark('creating') this.markPointId = this.app.mark('creating')
let shapeExists = false let shapeExists = false
@ -85,7 +82,7 @@ export class Pointing extends StateNode {
this.app.createShapes([ this.app.createShapes([
{ {
id, id,
type: this.shapeType, type: (this.parent as TLLineTool).shapeType,
x: currentPagePoint.x, x: currentPagePoint.x,
y: currentPagePoint.y, y: currentPagePoint.y,
}, },

View file

@ -1,63 +1,19 @@
import { import {
CLIENT_FIXUP_SCRIPT, CLIENT_FIXUP_SCRIPT,
TLAsset,
TLCamera,
TLDOCUMENT_ID, TLDOCUMENT_ID,
TLDocument, TLDefaultShape,
TLInstance, TLInstance,
TLInstanceId, TLInstanceId,
TLInstancePageState,
TLInstancePresence, TLInstancePresence,
TLPage,
TLRecord, TLRecord,
TLShape, TLShape,
TLStore, TLStore,
TLStoreProps, TLStoreProps,
TLUnknownShape,
TLUser, TLUser,
TLUserDocument,
TLUserId, TLUserId,
TLUserPresence, createTLSchema,
arrowShapeTypeMigrations,
arrowShapeTypeValidator,
bookmarkShapeTypeMigrations,
bookmarkShapeTypeValidator,
createIntegrityChecker,
defaultDerivePresenceState,
drawShapeTypeMigrations,
drawShapeTypeValidator,
embedShapeTypeMigrations,
embedShapeTypeValidator,
frameShapeTypeMigrations,
frameShapeTypeValidator,
geoShapeTypeMigrations,
geoShapeTypeValidator,
groupShapeTypeMigrations,
groupShapeTypeValidator,
imageShapeTypeMigrations,
imageShapeTypeValidator,
lineShapeTypeMigrations,
lineShapeTypeValidator,
noteShapeTypeMigrations,
noteShapeTypeValidator,
onValidationFailure,
rootShapeTypeMigrations,
storeMigrations,
textShapeTypeMigrations,
textShapeTypeValidator,
videoShapeTypeMigrations,
videoShapeTypeValidator,
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
import { import { Migrations, RecordType, Store, StoreSchema, StoreSnapshot } from '@tldraw/tlstore'
Migrations,
RecordType,
Store,
StoreSchema,
StoreSnapshot,
createRecordType,
defineMigrations,
} from '@tldraw/tlstore'
import { T } from '@tldraw/tlvalidate'
import { Signal } from 'signia' import { Signal } from 'signia'
import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil' import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil'
import { TLBookmarkUtil } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil' import { TLBookmarkUtil } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
@ -74,135 +30,69 @@ import { TLTextUtil } from '../app/shapeutils/TLTextUtil/TLTextUtil'
import { TLVideoUtil } from '../app/shapeutils/TLVideoUtil/TLVideoUtil' import { TLVideoUtil } from '../app/shapeutils/TLVideoUtil/TLVideoUtil'
import { StateNodeConstructor } from '../app/statechart/StateNode' import { StateNodeConstructor } from '../app/statechart/StateNode'
/** @public */ // Secret shape types that don't have a shape util yet
export type ValidatorsForShapes<T extends TLUnknownShape> = Record< type ShapeTypesNotImplemented = 'icon'
T['type'],
{ validate: (record: T) => T }
>
/** @public */ const DEFAULT_SHAPE_UTILS: {
export type MigrationsForShapes<T extends TLUnknownShape> = Record<T['type'], Migrations> [K in Exclude<TLDefaultShape['type'], ShapeTypesNotImplemented>]: TLShapeUtilConstructor<any>
} = {
type CustomShapeInfo<T extends TLUnknownShape> = { arrow: TLArrowUtil,
util: TLShapeUtilConstructor<any> bookmark: TLBookmarkUtil,
validator?: { validate: (record: T) => T } draw: TLDrawUtil,
migrations?: Migrations embed: TLEmbedUtil,
frame: TLFrameUtil,
geo: TLGeoUtil,
group: TLGroupUtil,
image: TLImageUtil,
line: TLLineUtil,
note: TLNoteUtil,
text: TLTextUtil,
video: TLVideoUtil,
} }
type UtilsForShapes<T extends TLUnknownShape> = Record<T['type'], TLShapeUtilConstructor<any>> /** @public */
export type TldrawEditorConfigOptions = {
type TldrawEditorConfigOptions<T extends TLUnknownShape = TLShape> = {
tools?: readonly StateNodeConstructor[] tools?: readonly StateNodeConstructor[]
shapes?: { [K in T['type']]: CustomShapeInfo<T> } shapes?: Record<
string,
{
util: TLShapeUtilConstructor<any>
validator?: { validate: <T>(record: T) => T }
migrations?: Migrations
}
>
/** @internal */ /** @internal */
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null> derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
} }
/** @public */ /** @public */
export class TldrawEditorConfig { export class TldrawEditorConfig {
static readonly default = new TldrawEditorConfig({}) // Custom tools
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>
readonly TLShape: RecordType<TLShape, 'type' | 'props' | 'index' | 'parentId'>
readonly tools: readonly StateNodeConstructor[] readonly tools: readonly StateNodeConstructor[]
// Custom shape utils // Custom shape utils
readonly shapeUtils: UtilsForShapes<TLShape> readonly shapeUtils: Record<TLShape['type'], TLShapeUtilConstructor<any>>
// Validators for shape subtypes
readonly shapeValidators: Record<TLShape['type'], T.Validator<any>>
// Migrations for shape subtypes
readonly shapeMigrations: MigrationsForShapes<TLShape>
constructor(opts: TldrawEditorConfigOptions) { // The record used for TLShape incorporating any custom shapes
const { shapes = [], tools = [], derivePresenceState } = opts readonly TLShape: RecordType<TLShape, 'type' | 'props' | 'index' | 'parentId'>
// The schema used for the store incorporating any custom shapes
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>
constructor(opts = {} as TldrawEditorConfigOptions) {
const { shapes = {}, tools = [], derivePresenceState } = opts
this.tools = tools this.tools = tools
this.shapeUtils = { this.shapeUtils = {
arrow: TLArrowUtil, ...DEFAULT_SHAPE_UTILS,
bookmark: TLBookmarkUtil, ...Object.fromEntries(Object.entries(shapes).map(([k, v]) => [k, v.util])),
draw: TLDrawUtil,
embed: TLEmbedUtil,
frame: TLFrameUtil,
geo: TLGeoUtil,
group: TLGroupUtil,
image: TLImageUtil,
line: TLLineUtil,
note: TLNoteUtil,
text: TLTextUtil,
video: TLVideoUtil,
} }
this.shapeMigrations = { this.storeSchema = createTLSchema({
arrow: arrowShapeTypeMigrations, customShapes: shapes,
bookmark: bookmarkShapeTypeMigrations, derivePresenceState: derivePresenceState,
draw: drawShapeTypeMigrations, })
embed: embedShapeTypeMigrations,
frame: frameShapeTypeMigrations,
geo: geoShapeTypeMigrations,
group: groupShapeTypeMigrations,
image: imageShapeTypeMigrations,
line: lineShapeTypeMigrations,
note: noteShapeTypeMigrations,
text: textShapeTypeMigrations,
video: videoShapeTypeMigrations,
}
this.shapeValidators = {
arrow: arrowShapeTypeValidator,
bookmark: bookmarkShapeTypeValidator,
draw: drawShapeTypeValidator,
embed: embedShapeTypeValidator,
frame: frameShapeTypeValidator,
geo: geoShapeTypeValidator,
group: groupShapeTypeValidator,
image: imageShapeTypeValidator,
line: lineShapeTypeValidator,
note: noteShapeTypeValidator,
text: textShapeTypeValidator,
video: videoShapeTypeValidator,
}
// Add custom shapes
for (const [type, shape] of Object.entries(shapes)) {
this.shapeUtils[type] = shape.util
this.shapeMigrations[type] = shape.migrations ?? defineMigrations({})
this.shapeValidators[type] = (shape.validator ?? T.any) as T.Validator<any>
}
const shapeRecord = createRecordType<TLShape>('shape', {
migrations: defineMigrations({
currentVersion: rootShapeTypeMigrations.currentVersion,
firstVersion: rootShapeTypeMigrations.firstVersion,
migrators: rootShapeTypeMigrations.migrators,
subTypeKey: 'type',
subTypeMigrations: this.shapeMigrations,
}),
validator: T.model('shape', T.union('type', { ...this.shapeValidators })),
scope: 'document',
}).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false }))
this.storeSchema = StoreSchema.create<TLRecord, TLStoreProps>(
{
asset: TLAsset,
camera: TLCamera,
document: TLDocument,
instance: TLInstance,
instance_page_state: TLInstancePageState,
page: TLPage,
shape: shapeRecord,
user: TLUser,
user_document: TLUserDocument,
user_presence: TLUserPresence,
instance_presence: TLInstancePresence,
},
{
snapshotMigrations: storeMigrations,
onValidationFailure,
createIntegrityChecker: createIntegrityChecker,
derivePresenceState: derivePresenceState ?? defaultDerivePresenceState,
}
)
this.TLShape = this.storeSchema.types.shape as RecordType< this.TLShape = this.storeSchema.types.shape as RecordType<
TLShape, TLShape,

View file

@ -0,0 +1,7 @@
import { TestApp } from './TestApp'
it('loads the test app', () => {
expect(() => {
new TestApp()
}).not.toThrow()
})

View file

@ -55,11 +55,13 @@ export const TEST_INSTANCE_ID = TLInstance.createCustomId('testInstance1')
export const TEST_USER_ID = TLUser.createCustomId('testUser1') export const TEST_USER_ID = TLUser.createCustomId('testUser1')
export class TestApp extends App { export class TestApp extends App {
constructor(options = {} as Partial<AppOptions>) { constructor(options = {} as Partial<Omit<AppOptions, 'store'>>) {
const elm = document.createElement('div') const elm = document.createElement('div')
elm.tabIndex = 0 elm.tabIndex = 0
const config = options.config ?? new TldrawEditorConfig()
super({ super({
store: (options.config ?? TldrawEditorConfig.default).createStore({ config,
store: config.createStore({
userId: TEST_USER_ID, userId: TEST_USER_ID,
instanceId: TEST_INSTANCE_ID, instanceId: TEST_INSTANCE_ID,
}), }),

View file

@ -21,14 +21,17 @@ afterEach(() => {
describe('<Tldraw />', () => { describe('<Tldraw />', () => {
it('Accepts fresh versions of store and calls `onMount` for each one', async () => { it('Accepts fresh versions of store and calls `onMount` for each one', async () => {
const initialStore = TldrawEditorConfig.default.createStore({ const config = new TldrawEditorConfig()
const initialStore = config.createStore({
instanceId: TLInstance.createCustomId('test'), instanceId: TLInstance.createCustomId('test'),
userId: TLUser.createCustomId('test'), userId: TLUser.createCustomId('test'),
}) })
const onMount = jest.fn() const onMount = jest.fn()
const rendered = render( const rendered = render(
<TldrawEditor store={initialStore} onMount={onMount} autoFocus> <TldrawEditor config={config} store={initialStore} onMount={onMount} autoFocus>
<div data-testid="canvas-1" /> <div data-testid="canvas-1" />
</TldrawEditor> </TldrawEditor>
) )
@ -40,7 +43,7 @@ describe('<Tldraw />', () => {
// re-render with the same store: // re-render with the same store:
rendered.rerender( rendered.rerender(
<TldrawEditor store={initialStore} onMount={onMount} autoFocus> <TldrawEditor config={config} store={initialStore} onMount={onMount} autoFocus>
<div data-testid="canvas-2" /> <div data-testid="canvas-2" />
</TldrawEditor> </TldrawEditor>
) )
@ -49,12 +52,12 @@ describe('<Tldraw />', () => {
expect(onMount).toHaveBeenCalledTimes(1) expect(onMount).toHaveBeenCalledTimes(1)
// re-render with a new store: // re-render with a new store:
const newStore = TldrawEditorConfig.default.createStore({ const newStore = config.createStore({
instanceId: TLInstance.createCustomId('test'), instanceId: TLInstance.createCustomId('test'),
userId: TLUser.createCustomId('test'), userId: TLUser.createCustomId('test'),
}) })
rendered.rerender( rendered.rerender(
<TldrawEditor store={newStore} onMount={onMount} autoFocus> <TldrawEditor config={config} store={newStore} onMount={onMount} autoFocus>
<div data-testid="canvas-3" /> <div data-testid="canvas-3" />
</TldrawEditor> </TldrawEditor>
) )

View file

@ -1076,19 +1076,6 @@ export interface LegacyTldrawDocument {
/* ------------------ Translations ------------------ */ /* ------------------ Translations ------------------ */
// const v1ShapeTypesToV2ShapeTypes: Record<TDShapeType, TLShapeType> = {
// [TDShapeType.Rectangle]: 'geo',
// [TDShapeType.Ellipse]: 'geo',
// [TDShapeType.Text]: 'text',
// [TDShapeType.Image]: 'image',
// [TDShapeType.Video]: 'video',
// [TDShapeType.Group]: 'group',
// [TDShapeType.Arrow]: 'arrow',
// [TDShapeType.Sticky]: 'note',
// [TDShapeType.Draw]: 'draw',
// [TDShapeType.Triangle]: 'geo',
// }
const v1ColorsToV2Colors: Record<ColorStyle, TLColorType> = { const v1ColorsToV2Colors: Record<ColorStyle, TLColorType> = {
[ColorStyle.White]: 'black', [ColorStyle.White]: 'black',
[ColorStyle.Black]: 'black', [ColorStyle.Black]: 'black',

View file

@ -208,7 +208,7 @@ export async function parseAndLoadDocument(
forceDarkMode?: boolean forceDarkMode?: boolean
) { ) {
const parseFileResult = parseTldrawJsonFile({ const parseFileResult = parseTldrawJsonFile({
config: TldrawEditorConfig.default, config: new TldrawEditorConfig(),
json: document, json: document,
instanceId: app.instanceId, instanceId: app.instanceId,
userId: app.userId, userId: app.userId,

View file

@ -17,14 +17,14 @@ 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(TldrawEditorConfig.default, 'not json') const result = parseTldrawJsonFile(new TldrawEditorConfig(), '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 doesnt look like a tldraw file', () => { it('returns an error if the file doesnt look like a tldraw file', () => {
const result = parseTldrawJsonFile( const result = parseTldrawJsonFile(
TldrawEditorConfig.default, new TldrawEditorConfig(),
JSON.stringify({ not: 'a tldraw file' }) JSON.stringify({ not: 'a tldraw file' })
) )
assert(!result.ok) assert(!result.ok)
@ -33,10 +33,10 @@ describe('parseTldrawJsonFile', () => {
it('returns an error if the file version is too old', () => { it('returns an error if the file version is too old', () => {
const result = parseTldrawJsonFile( const result = parseTldrawJsonFile(
TldrawEditorConfig.default, new TldrawEditorConfig(),
serialize({ serialize({
tldrawFileFormatVersion: 0, tldrawFileFormatVersion: 0,
schema: TldrawEditorConfig.default.storeSchema.serialize(), schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [], records: [],
}) })
) )
@ -46,10 +46,10 @@ 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 result = parseTldrawJsonFile( const result = parseTldrawJsonFile(
TldrawEditorConfig.default, new TldrawEditorConfig(),
serialize({ serialize({
tldrawFileFormatVersion: 100, tldrawFileFormatVersion: 100,
schema: TldrawEditorConfig.default.storeSchema.serialize(), schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [], records: [],
}) })
) )
@ -58,10 +58,10 @@ describe('parseTldrawJsonFile', () => {
}) })
it('returns an error if migrations fail', () => { it('returns an error if migrations fail', () => {
const serializedSchema = TldrawEditorConfig.default.storeSchema.serialize() const serializedSchema = new TldrawEditorConfig().storeSchema.serialize()
serializedSchema.storeVersion = 100 serializedSchema.storeVersion = 100
const result = parseTldrawJsonFile( const result = parseTldrawJsonFile(
TldrawEditorConfig.default, new TldrawEditorConfig(),
serialize({ serialize({
tldrawFileFormatVersion: 1, tldrawFileFormatVersion: 1,
schema: serializedSchema, schema: serializedSchema,
@ -72,10 +72,10 @@ 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 = TldrawEditorConfig.default.storeSchema.serialize() const serializedSchema2 = new TldrawEditorConfig().storeSchema.serialize()
serializedSchema2.recordVersions.shape.version = 100 serializedSchema2.recordVersions.shape.version = 100
const result2 = parseTldrawJsonFile( const result2 = parseTldrawJsonFile(
TldrawEditorConfig.default, new TldrawEditorConfig(),
serialize({ serialize({
tldrawFileFormatVersion: 1, tldrawFileFormatVersion: 1,
schema: serializedSchema2, schema: serializedSchema2,
@ -90,10 +90,10 @@ describe('parseTldrawJsonFile', () => {
it('returns an error if a record is invalid', () => { it('returns an error if a record is invalid', () => {
const result = parseTldrawJsonFile( const result = parseTldrawJsonFile(
TldrawEditorConfig.default, new TldrawEditorConfig(),
serialize({ serialize({
tldrawFileFormatVersion: 1, tldrawFileFormatVersion: 1,
schema: TldrawEditorConfig.default.storeSchema.serialize(), schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [ records: [
{ {
typeName: 'shape', typeName: 'shape',
@ -113,10 +113,10 @@ describe('parseTldrawJsonFile', () => {
it('returns a store if the file is valid', () => { it('returns a store if the file is valid', () => {
const result = parseTldrawJsonFile( const result = parseTldrawJsonFile(
TldrawEditorConfig.default, new TldrawEditorConfig(),
serialize({ serialize({
tldrawFileFormatVersion: 1, tldrawFileFormatVersion: 1,
schema: TldrawEditorConfig.default.storeSchema.serialize(), schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [], records: [],
}) })
) )

View file

@ -8,9 +8,10 @@ import { TldrawEditorProps } from '@tldraw/editor';
import { TldrawUiContextProviderProps } from '@tldraw/ui'; import { TldrawUiContextProviderProps } from '@tldraw/ui';
// @public (undocumented) // @public (undocumented)
export function Tldraw(props: Omit<TldrawEditorProps, 'store'> & TldrawUiContextProviderProps & { export function Tldraw(props: Omit<TldrawEditorProps, 'config' | 'store'> & TldrawUiContextProviderProps & {
persistenceKey?: string; persistenceKey?: string;
hideUi?: boolean; hideUi?: boolean;
config?: TldrawEditorProps['config'];
}): JSX.Element; }): JSX.Element;

View file

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

View file

@ -103,6 +103,12 @@ export function createShapeValidator<Type extends string, Props extends object>(
props: Props; props: Props;
}>; }>;
// @public
export function createTLSchema<T extends TLUnknownShape>(opts?: {
customShapes?: { [K in T["type"]]: CustomShapeInfo<T>; } | undefined;
derivePresenceState?: ((store: TLStore) => Signal<null | TLInstancePresence>) | undefined;
}): StoreSchema<TLRecord, TLStoreProps>;
// @public (undocumented) // @public (undocumented)
export const cursorTypeValidator: T.Validator<string>; export const cursorTypeValidator: T.Validator<string>;
@ -722,6 +728,9 @@ export interface TLDashStyle extends TLBaseStyle {
// @public (undocumented) // @public (undocumented)
export type TLDashType = SetValue<typeof TL_DASH_TYPES>; export type TLDashType = SetValue<typeof TL_DASH_TYPES>;
// @public
export type TLDefaultShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEmbedShape | TLFrameShape | TLGeoShape | TLGroupShape | TLIconShape | TLImageShape | TLLineShape | TLNoteShape | TLTextShape | TLVideoShape;
// @public // @public
export interface TLDocument extends BaseRecord<'document'> { export interface TLDocument extends BaseRecord<'document'> {
// (undocumented) // (undocumented)
@ -1137,7 +1146,7 @@ export type TLScribble = {
}; };
// @public // @public
export type TLShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEmbedShape | TLFrameShape | TLGeoShape | TLGroupShape | TLIconShape | TLImageShape | TLLineShape | TLNoteShape | TLTextShape | TLUnknownShape | TLVideoShape; export type TLShape = TLDefaultShape | TLUnknownShape;
// @public (undocumented) // @public (undocumented)
export type TLShapeId = ID<TLBaseShape<any, any>>; export type TLShapeId = ID<TLBaseShape<any, any>>;
@ -1155,9 +1164,6 @@ export type TLShapeProp = keyof TLShapeProps;
// @public (undocumented) // @public (undocumented)
export type TLShapeProps = SmooshedUnionObject<TLShape['props']>; export type TLShapeProps = SmooshedUnionObject<TLShape['props']>;
// @public (undocumented)
export type TLShapeType = TLShape['type'];
// @public (undocumented) // @public (undocumented)
export interface TLSizeStyle extends TLBaseStyle { export interface TLSizeStyle extends TLBaseStyle {
// (undocumented) // (undocumented)
@ -1252,7 +1258,7 @@ export type TLTextShapeProps = {
// @public (undocumented) // @public (undocumented)
export type TLUiColorType = SetValue<typeof TL_UI_COLOR_TYPES>; export type TLUiColorType = SetValue<typeof TL_UI_COLOR_TYPES>;
// @public (undocumented) // @public
export type TLUnknownShape = TLBaseShape<string, object>; export type TLUnknownShape = TLBaseShape<string, object>;
// @public // @public

View file

@ -0,0 +1,134 @@
import { Migrations, StoreSchema, createRecordType, defineMigrations } from '@tldraw/tlstore'
import { T } from '@tldraw/tlvalidate'
import { Signal } from 'signia'
import { TLRecord } from './TLRecord'
import { TLStore, TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLStore'
import { defaultDerivePresenceState } from './defaultDerivePresenceState'
import { TLAsset } from './records/TLAsset'
import { TLCamera } from './records/TLCamera'
import { TLDocument } from './records/TLDocument'
import { TLInstance } from './records/TLInstance'
import { TLInstancePageState } from './records/TLInstancePageState'
import { TLInstancePresence } from './records/TLInstancePresence'
import { TLPage } from './records/TLPage'
import { TLShape, TLUnknownShape, rootShapeTypeMigrations } from './records/TLShape'
import { TLUser } from './records/TLUser'
import { TLUserDocument } from './records/TLUserDocument'
import { TLUserPresence } from './records/TLUserPresence'
import { storeMigrations } from './schema'
import { arrowShapeTypeMigrations, arrowShapeTypeValidator } from './shapes/TLArrowShape'
import { bookmarkShapeTypeMigrations, bookmarkShapeTypeValidator } from './shapes/TLBookmarkShape'
import { drawShapeTypeMigrations, drawShapeTypeValidator } from './shapes/TLDrawShape'
import { embedShapeTypeMigrations, embedShapeTypeValidator } from './shapes/TLEmbedShape'
import { frameShapeTypeMigrations, frameShapeTypeValidator } from './shapes/TLFrameShape'
import { geoShapeTypeMigrations, geoShapeTypeValidator } from './shapes/TLGeoShape'
import { groupShapeTypeMigrations, groupShapeTypeValidator } from './shapes/TLGroupShape'
import { imageShapeTypeMigrations, imageShapeTypeValidator } from './shapes/TLImageShape'
import { lineShapeTypeMigrations, lineShapeTypeValidator } from './shapes/TLLineShape'
import { noteShapeTypeMigrations, noteShapeTypeValidator } from './shapes/TLNoteShape'
import { textShapeTypeMigrations, textShapeTypeValidator } from './shapes/TLTextShape'
import { videoShapeTypeMigrations, videoShapeTypeValidator } from './shapes/TLVideoShape'
type DefaultShapeInfo<T extends TLShape> = {
validator: T.Validator<T>
migrations: Migrations
}
const DEFAULT_SHAPES: { [K in TLShape['type']]: DefaultShapeInfo<Extract<TLShape, { type: K }>> } =
{
arrow: { migrations: arrowShapeTypeMigrations, validator: arrowShapeTypeValidator },
bookmark: { migrations: bookmarkShapeTypeMigrations, validator: bookmarkShapeTypeValidator },
draw: { migrations: drawShapeTypeMigrations, validator: drawShapeTypeValidator },
embed: { migrations: embedShapeTypeMigrations, validator: embedShapeTypeValidator },
frame: { migrations: frameShapeTypeMigrations, validator: frameShapeTypeValidator },
geo: { migrations: geoShapeTypeMigrations, validator: geoShapeTypeValidator },
group: { migrations: groupShapeTypeMigrations, validator: groupShapeTypeValidator },
image: { migrations: imageShapeTypeMigrations, validator: imageShapeTypeValidator },
line: { migrations: lineShapeTypeMigrations, validator: lineShapeTypeValidator },
note: { migrations: noteShapeTypeMigrations, validator: noteShapeTypeValidator },
text: { migrations: textShapeTypeMigrations, validator: textShapeTypeValidator },
video: { migrations: videoShapeTypeMigrations, validator: videoShapeTypeValidator },
}
type CustomShapeInfo<T extends TLUnknownShape> = {
validator?: { validate: (record: T) => T }
migrations?: Migrations
}
/**
* Create a store schema for a tldraw store that includes all the default shapes together with any custom shapes.
* @public */
export function createTLSchema<T extends TLUnknownShape>(
opts = {} as {
customShapes?: { [K in T['type']]: CustomShapeInfo<T> }
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
}
) {
const { customShapes = {}, derivePresenceState } = opts
const defaultShapeSubTypeEntries = Object.entries(DEFAULT_SHAPES) as [
TLShape['type'],
DefaultShapeInfo<TLShape>
][]
const customShapeSubTypeEntries = Object.entries(customShapes) as [
T['type'],
CustomShapeInfo<T>
][]
// Create a shape record that incorporates the defeault 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(
'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', {
migrations: defineMigrations({
currentVersion: rootShapeTypeMigrations.currentVersion,
firstVersion: rootShapeTypeMigrations.firstVersion,
migrators: rootShapeTypeMigrations.migrators,
subTypeKey: 'type',
subTypeMigrations: shapeSubTypeMigrationsWithCustomSubTypeMigrations,
}),
validator: validatorWithCustomShapeValidators,
scope: 'document',
}).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false }))
return StoreSchema.create<TLRecord, TLStoreProps>(
{
asset: TLAsset,
camera: TLCamera,
document: TLDocument,
instance: TLInstance,
instance_page_state: TLInstancePageState,
page: TLPage,
shape: shapeRecord,
user: TLUser,
user_document: TLUserDocument,
user_presence: TLUserPresence,
instance_presence: TLInstancePresence,
},
{
snapshotMigrations: storeMigrations,
onValidationFailure,
createIntegrityChecker: createIntegrityChecker,
derivePresenceState: derivePresenceState ?? defaultDerivePresenceState,
}
)
}

View file

@ -24,6 +24,7 @@ export {
type TLVideoAsset, type TLVideoAsset,
} from './assets/TLVideoAsset' } from './assets/TLVideoAsset'
export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation' export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation'
export { createTLSchema } from './createTLSchema'
export { defaultDerivePresenceState } from './defaultDerivePresenceState' export { defaultDerivePresenceState } from './defaultDerivePresenceState'
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'
@ -58,6 +59,7 @@ export {
isShape, isShape,
isShapeId, isShapeId,
rootShapeTypeMigrations, rootShapeTypeMigrations,
type TLDefaultShape,
type TLNullableShapeProps, type TLNullableShapeProps,
type TLParentId, type TLParentId,
type TLShape, type TLShape,
@ -65,7 +67,6 @@ export {
type TLShapePartial, type TLShapePartial,
type TLShapeProp, type TLShapeProp,
type TLShapeProps, type TLShapeProps,
type TLShapeType,
type TLUnknownShape, type TLUnknownShape,
} from './records/TLShape' } from './records/TLShape'
export { TLUser, userTypeValidator, type TLUserId } from './records/TLUser' export { TLUser, userTypeValidator, type TLUserId } from './records/TLUser'

View file

@ -17,15 +17,11 @@ import { TLVideoShape } from '../shapes/TLVideoShape'
import { SmooshedUnionObject } from '../util-types' import { SmooshedUnionObject } from '../util-types'
import { TLPageId } from './TLPage' import { TLPageId } from './TLPage'
/** @public */
export type TLUnknownShape = TLBaseShape<string, object>
/** /**
* TLShape * The default set of shapes that are available in the editor.
* *
* @public * @public */
*/ export type TLDefaultShape =
export type TLShape =
| TLArrowShape | TLArrowShape
| TLBookmarkShape | TLBookmarkShape
| TLDrawShape | TLDrawShape
@ -38,11 +34,21 @@ export type TLShape =
| TLNoteShape | TLNoteShape
| TLTextShape | TLTextShape
| TLVideoShape | TLVideoShape
| TLUnknownShape
| TLIconShape | TLIconShape
/** @public */ /**
export type TLShapeType = TLShape['type'] * A type for a shape that is available in the editor but whose type is
* unknowneither one of the editor's default shapes or else a custom shape.
*
* @public */
export type TLUnknownShape = TLBaseShape<string, object>
/**
* The set of all shapes that are available in the editor, including unknown shapes.
*
* @public
*/
export type TLShape = TLDefaultShape | TLUnknownShape
/** @public */ /** @public */
export type TLShapePartial<T extends TLShape = TLShape> = T extends T export type TLShapePartial<T extends TLShape = TLShape> = T extends T

View file

@ -101,7 +101,7 @@ export function useLocalSyncClient({ universalPersistenceKey, instanceId, userId
universalPersistenceKey: string; universalPersistenceKey: string;
instanceId: TLInstanceId; instanceId: TLInstanceId;
userId: TLUserId; userId: TLUserId;
config?: TldrawEditorConfig; config: TldrawEditorConfig;
}): SyncedStore; }): SyncedStore;
// (No @packageDocumentation comment for this package) // (No @packageDocumentation comment for this package)

View file

@ -34,7 +34,7 @@ function testClient(
userId: TLUserId = TLUser.createCustomId('test'), userId: TLUserId = TLUser.createCustomId('test'),
channel = new BroadcastChannelMock('test') channel = new BroadcastChannelMock('test')
) { ) {
const store = TldrawEditorConfig.default.createStore({ const store = new TldrawEditorConfig().createStore({
userId, userId,
instanceId, instanceId,
}) })

View file

@ -14,12 +14,12 @@ export function useLocalSyncClient({
universalPersistenceKey, universalPersistenceKey,
instanceId, instanceId,
userId, userId,
config = TldrawEditorConfig.default, config,
}: { }: {
universalPersistenceKey: string universalPersistenceKey: string
instanceId: TLInstanceId instanceId: TLInstanceId
userId: TLUserId userId: TLUserId
config?: TldrawEditorConfig config: TldrawEditorConfig
}): SyncedStore { }): SyncedStore {
const [state, setState] = useState<{ id: string; syncedStore: SyncedStore } | null>(null) const [state, setState] = useState<{ id: string; syncedStore: SyncedStore } | null>(null)