diff --git a/apps/docs/content/docs/persistence.mdx b/apps/docs/content/docs/persistence.mdx index d22eda77d..e899e356a 100644 --- a/apps/docs/content/docs/persistence.mdx +++ b/apps/docs/content/docs/persistence.mdx @@ -15,4 +15,4 @@ keywords: Coming soon. -See the [tldraw repository](https://github.com/tldraw/tldraw) for an example of how to use the `@tldraw/tlsync-client` library to persist and sync between tabs. +See the [tldraw repository](https://github.com/tldraw/tldraw) for an example of how to use persistence with the `@tldraw/tldraw` or `@tldraw/editor` libraries. diff --git a/apps/docs/package.json b/apps/docs/package.json index 2c4a42482..2c67fd4b2 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -64,7 +64,6 @@ "@tldraw/tldraw": "workspace:*", "@tldraw/tlschema": "workspace:*", "@tldraw/tlstore": "workspace:*", - "@tldraw/tlsync-client": "workspace:*", "@tldraw/tlvalidate": "workspace:*", "@tldraw/ui": "workspace:*", "lazyrepo": "0.0.0-alpha.26", diff --git a/apps/examples/e2e/shared-e2e.ts b/apps/examples/e2e/shared-e2e.ts index abacd7997..0ab8cb724 100644 --- a/apps/examples/e2e/shared-e2e.ts +++ b/apps/examples/e2e/shared-e2e.ts @@ -33,7 +33,7 @@ export async function cleanup({ page }: PlaywrightTestArgs) { } export async function setupPage(page: PlaywrightTestArgs['page']) { - await page.goto('http://localhost:5420/e2e') + await page.goto('http://localhost:5420/end-to-end') await page.waitForSelector('.tl-canvas') await page.evaluate(() => (app.enableAnimations = false)) } diff --git a/apps/examples/e2e/tests/test-routes.spec.ts b/apps/examples/e2e/tests/test-routes.spec.ts new file mode 100644 index 000000000..9eb7c1950 --- /dev/null +++ b/apps/examples/e2e/tests/test-routes.spec.ts @@ -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') + }) +}) diff --git a/apps/examples/src/13-store/StoreEventsExample.tsx b/apps/examples/src/13-store-events/StoreEventsExample.tsx similarity index 100% rename from apps/examples/src/13-store/StoreEventsExample.tsx rename to apps/examples/src/13-store-events/StoreEventsExample.tsx diff --git a/apps/examples/src/14-persistence/PersistenceExample.tsx b/apps/examples/src/14-persistence/PersistenceExample.tsx index d1e8108c8..3d9d9f604 100644 --- a/apps/examples/src/14-persistence/PersistenceExample.tsx +++ b/apps/examples/src/14-persistence/PersistenceExample.tsx @@ -1,37 +1,21 @@ -import { - Canvas, - ContextMenu, - TAB_ID, - TldrawEditor, - TldrawEditorConfig, - TldrawUi, -} from '@tldraw/tldraw' +import { Canvas, ContextMenu, TAB_ID, TldrawEditor, TldrawUi, createTLStore } from '@tldraw/tldraw' import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/ui.css' import { throttle } from '@tldraw/utils' -import { useEffect, useState } from 'react' +import { useLayoutEffect, useState } from 'react' const PERSISTENCE_KEY = 'example-3' -const config = new TldrawEditorConfig() -const instanceId = TAB_ID -const store = config.createStore({ instanceId }) export default function PersistenceExample() { - const [state, setState] = useState< - | { - name: 'loading' - } - | { - name: 'ready' - } - | { - name: 'error' - error: string - } - >({ name: 'loading', error: undefined }) + const [store] = useState(() => createTLStore({ instanceId: TAB_ID })) + const [loadingStore, setLoadingStore] = useState< + { status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string } + >({ + status: 'loading', + }) - useEffect(() => { - setState({ name: 'loading' }) + useLayoutEffect(() => { + setLoadingStore({ status: 'loading' }) // Get persisted data from local storage const persistedSnapshot = localStorage.getItem(PERSISTENCE_KEY) @@ -40,29 +24,28 @@ export default function PersistenceExample() { try { const snapshot = JSON.parse(persistedSnapshot) store.loadSnapshot(snapshot) - setState({ name: 'ready' }) - } catch (e: any) { - setState({ name: 'error', error: e.message }) // Something went wrong + setLoadingStore({ status: 'ready' }) + } catch (error: any) { + setLoadingStore({ status: 'error', error: error.message }) // Something went wrong } } else { - setState({ name: 'ready' }) // Nothing persisted, continue with the empty store + setLoadingStore({ status: 'ready' }) // Nothing persisted, continue with the empty store } - const persist = throttle(() => { - // Each time the store changes, persist the store snapshot - const snapshot = store.getSnapshot() - localStorage.setItem(PERSISTENCE_KEY, JSON.stringify(snapshot)) - }, 1000) - // Each time the store changes, run the (debounced) persist function - const cleanupFn = store.listen(persist) + const cleanupFn = store.listen( + throttle(() => { + const snapshot = store.getSnapshot() + localStorage.setItem(PERSISTENCE_KEY, JSON.stringify(snapshot)) + }, 500) + ) return () => { cleanupFn() } - }, []) + }, [store]) - if (state.name === 'loading') { + if (loadingStore.status === 'loading') { return (

Loading...

@@ -70,18 +53,18 @@ export default function PersistenceExample() { ) } - if (state.name === 'error') { + if (loadingStore.status === 'error') { return (

Error!

-

{state.error}

+

{loadingStore.error}

) } return (
- + diff --git a/apps/examples/src/2-api/APIExample.tsx b/apps/examples/src/2-api/APIExample.tsx index f81c3b7f4..fab43f928 100644 --- a/apps/examples/src/2-api/APIExample.tsx +++ b/apps/examples/src/2-api/APIExample.tsx @@ -10,7 +10,7 @@ import { useEffect } from 'react' // component and all shapes, tools, and UI components use this instance to // send events, observe changes, and perform actions. -export default function Example() { +export default function APIExample() { const handleMount = (app: App) => { // Create a shape id const id = app.createShapeId('hello') diff --git a/apps/examples/src/3-custom-config/CardShape.ts b/apps/examples/src/3-custom-config/CardShape.ts new file mode 100644 index 000000000..367c3b75f --- /dev/null +++ b/apps/examples/src/3-custom-config/CardShape.ts @@ -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 + } +> diff --git a/apps/examples/src/3-custom-config/CardTool.ts b/apps/examples/src/3-custom-config/CardTool.ts new file mode 100644 index 000000000..2312f9a18 --- /dev/null +++ b/apps/examples/src/3-custom-config/CardTool.ts @@ -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' +} diff --git a/apps/examples/src/3-custom-config/CardUtil.tsx b/apps/examples/src/3-custom-config/CardUtil.tsx new file mode 100644 index 000000000..e0cc1155f --- /dev/null +++ b/apps/examples/src/3-custom-config/CardUtil.tsx @@ -0,0 +1,46 @@ +import { HTMLContainer, TLBoxUtil } from '@tldraw/tldraw' +import { CardShape } from './CardShape' + +export class CardUtil extends TLBoxUtil { + // 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 ( + + {bounds.w.toFixed()}x{bounds.h.toFixed()} + + ) + } + + // Indicator — used when hovering over a shape or when it's selected; must return only SVG elements here + indicator(shape: CardShape) { + return + } +} diff --git a/apps/examples/src/3-custom-config/CustomConfigExample.tsx b/apps/examples/src/3-custom-config/CustomConfigExample.tsx index 6d02314d0..54884267a 100644 --- a/apps/examples/src/3-custom-config/CustomConfigExample.tsx +++ b/apps/examples/src/3-custom-config/CustomConfigExample.tsx @@ -1,120 +1,25 @@ -import { - HTMLContainer, - MenuGroup, - menuItem, - TLBaseShape, - TLBoxTool, - TLBoxUtil, - Tldraw, - TldrawEditorConfig, - TLOpacityType, - toolbarItem, -} from '@tldraw/tldraw' +import { MenuGroup, Tldraw, menuItem, toolbarItem } from '@tldraw/tldraw' import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/ui.css' +import { CardTool } from './CardTool' +import { CardUtil } from './CardUtil' -// Let's make a custom shape called a Card. +const shapes = { card: { util: CardUtil } } +const tools = [CardTool] -// Shape Type -// ---------- -// The shape type defines the card's type (`card`) and its props. -// Every shape needs an opacity prop (for now), but other than that -// you can add whatever you want, so long as it's JSON serializable. -type CardShape = TLBaseShape< - 'card', - { - w: number - h: number - opacity: TLOpacityType - } -> - -// Shape Util -// ---------- -// The CardUtil class is used by the app to answer questions about a -// shape of the 'card' type. For example, what is the default props -// for this shape? What should we render for it, or for its indicator? -class CardUtil extends TLBoxUtil { - 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 ( - - {/* Anything you want can go here—it's a regular React component */} - {bounds.w.toFixed()}x{bounds.h.toFixed()} - - ) - } - - // 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 - } -} - -// Tool -// ---- -// Because the card tool can be just a rectangle, we can extend the -// TLBoxTool class. This gives us a lot of functionality for free. -export class CardTool extends TLBoxTool { - static override id = 'card' - static override initial = 'idle' - override shapeType = 'card' -} - -// Finally, collect the custom tools and shapes into a config object -const customTldrawConfig = new TldrawEditorConfig({ - tools: [CardTool], - shapes: { - card: { - util: CardUtil, - }, - }, -}) - -// ... and we can make our custom shape example! -export default function Example() { +export default function CustomConfigExample() { return (
- + diff --git a/apps/examples/src/5-exploded/ExplodedExample.tsx b/apps/examples/src/5-exploded/ExplodedExample.tsx index 0c540996c..ad85d9400 100644 --- a/apps/examples/src/5-exploded/ExplodedExample.tsx +++ b/apps/examples/src/5-exploded/ExplodedExample.tsx @@ -1,30 +1,11 @@ -import { - Canvas, - ContextMenu, - InstanceRecordType, - TldrawEditor, - TldrawEditorConfig, - TldrawUi, - useLocalSyncClient, -} from '@tldraw/tldraw' +import { Canvas, ContextMenu, TldrawEditor, TldrawUi } from '@tldraw/tldraw' import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/ui.css' -const instanceId = InstanceRecordType.createCustomId('example') - -// for custom config, see 3-custom-config -const config = new TldrawEditorConfig() - -export default function Example() { - const syncedStore = useLocalSyncClient({ - config, - instanceId, - universalPersistenceKey: 'exploded-example', - }) - +export default function ExplodedExample() { return (
- + diff --git a/apps/examples/src/7-multiple/MultipleExample.tsx b/apps/examples/src/7-multiple/MultipleExample.tsx index ce8362547..87726e24b 100644 --- a/apps/examples/src/7-multiple/MultipleExample.tsx +++ b/apps/examples/src/7-multiple/MultipleExample.tsx @@ -2,7 +2,7 @@ import { Tldraw } from '@tldraw/tldraw' import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/ui.css' -export default function Example() { +export default function MultipleExample() { return (
-
Shape error! {String(error)}
, - }} - // 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() - }} - /> -
- ) -} - -// 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 { - 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, - }, - }, -}) diff --git a/apps/examples/src/8-error-boundary/ErrorBoundaryExample.tsx b/apps/examples/src/8-error-boundary/ErrorBoundaryExample.tsx new file mode 100644 index 000000000..62c5d8d14 --- /dev/null +++ b/apps/examples/src/8-error-boundary/ErrorBoundaryExample.tsx @@ -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 ( +
+
Shape error! {String(error)}
, // 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() + }} + /> +
+ ) +} diff --git a/apps/examples/src/8-error-boundary/ErrorShape.ts b/apps/examples/src/8-error-boundary/ErrorShape.ts new file mode 100644 index 000000000..37e90a5d1 --- /dev/null +++ b/apps/examples/src/8-error-boundary/ErrorShape.ts @@ -0,0 +1,3 @@ +import { TLBaseShape } from '@tldraw/tldraw' + +export type ErrorShape = TLBaseShape<'error', { w: number; h: number; message: string }> diff --git a/apps/examples/src/8-error-boundary/ErrorUtil.ts b/apps/examples/src/8-error-boundary/ErrorUtil.ts new file mode 100644 index 000000000..13a800ae4 --- /dev/null +++ b/apps/examples/src/8-error-boundary/ErrorUtil.ts @@ -0,0 +1,17 @@ +import { TLBoxUtil } from '@tldraw/tldraw' +import { ErrorShape } from './ErrorShape' + +export class ErrorUtil extends TLBoxUtil { + 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!`) + } +} diff --git a/apps/examples/src/end-to-end/ForEndToEndTests.tsx b/apps/examples/src/end-to-end/end-to-end.tsx similarity index 91% rename from apps/examples/src/end-to-end/ForEndToEndTests.tsx rename to apps/examples/src/end-to-end/end-to-end.tsx index 83dd598a5..ccd5b6025 100644 --- a/apps/examples/src/end-to-end/ForEndToEndTests.tsx +++ b/apps/examples/src/end-to-end/end-to-end.tsx @@ -2,9 +2,8 @@ import { Tldraw } from '@tldraw/tldraw' import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/ui.css' -export default function ForEndToEndTests() { +export default function EndToEnd() { ;(window as any).__tldraw_editor_events = [] - return (
, }, { - path: '/custom', + path: '/custom-config', element: , }, { @@ -92,8 +95,8 @@ export const allExamples: Example[] = [ element: , }, { - path: '/e2e', - element: , + path: '/end-to-end', + element: , }, ] diff --git a/apps/vscode/editor/package.json b/apps/vscode/editor/package.json index 109c03f6d..6935727e6 100644 --- a/apps/vscode/editor/package.json +++ b/apps/vscode/editor/package.json @@ -37,7 +37,6 @@ "@tldraw/editor": "workspace:*", "@tldraw/file-format": "workspace:*", "@tldraw/tldraw": "workspace:*", - "@tldraw/tlsync-client": "workspace:*", "@tldraw/ui": "workspace:*", "@tldraw/utils": "workspace:*", "@types/fs-extra": "^11.0.1", diff --git a/apps/vscode/editor/src/ChangeResponder.tsx b/apps/vscode/editor/src/ChangeResponder.tsx index 53c09a279..34f0bfb8b 100644 --- a/apps/vscode/editor/src/ChangeResponder.tsx +++ b/apps/vscode/editor/src/ChangeResponder.tsx @@ -1,4 +1,4 @@ -import { SyncedStore, TLInstanceId, useApp } from '@tldraw/editor' +import { useApp } from '@tldraw/editor' import { parseAndLoadDocument, serializeTldrawJson } from '@tldraw/file-format' import { useDefaultHelpers } from '@tldraw/ui' import { debounce } from '@tldraw/utils' @@ -9,13 +9,7 @@ import { vscode } from './utils/vscode' // @ts-ignore import type { VscodeMessage } from '../../messages' -export const ChangeResponder = ({ - syncedStore, - instanceId, -}: { - syncedStore: SyncedStore - instanceId: TLInstanceId -}) => { +export const ChangeResponder = () => { const app = useApp() const { addToast, clearToasts, msg } = useDefaultHelpers() @@ -44,19 +38,17 @@ export const ChangeResponder = ({ clearToasts() window.removeEventListener('message', handleMessage) } - }, [app, instanceId, msg, addToast, clearToasts]) + }, [app, msg, addToast, clearToasts]) React.useEffect(() => { // When the history changes, send the new file contents to VSCode const handleChange = debounce(async () => { - if (syncedStore.store) { - vscode.postMessage({ - type: 'vscode:editor-updated', - data: { - fileContents: await serializeTldrawJson(syncedStore.store), - }, - }) - } + vscode.postMessage({ + type: 'vscode:editor-updated', + data: { + fileContents: await serializeTldrawJson(app.store), + }, + }) }, 250) vscode.postMessage({ @@ -69,7 +61,7 @@ export const ChangeResponder = ({ handleChange() app.off('change-history', handleChange) } - }, [app, syncedStore, instanceId]) + }, [app]) return null } diff --git a/apps/vscode/editor/src/FileOpen.tsx b/apps/vscode/editor/src/FileOpen.tsx index 17ad6d0e0..850c958d8 100644 --- a/apps/vscode/editor/src/FileOpen.tsx +++ b/apps/vscode/editor/src/FileOpen.tsx @@ -1,4 +1,4 @@ -import { TLInstanceId, useApp } from '@tldraw/editor' +import { useApp } from '@tldraw/editor' import { parseAndLoadDocument } from '@tldraw/file-format' import { useDefaultHelpers } from '@tldraw/ui' import React from 'react' @@ -6,10 +6,8 @@ import { vscode } from './utils/vscode' export function FileOpen({ fileContents, - instanceId, forceDarkMode, }: { - instanceId: TLInstanceId fileContents: string forceDarkMode: boolean }) { @@ -42,7 +40,7 @@ export function FileOpen({ return () => { clearToasts() } - }, [fileContents, app, instanceId, addToast, msg, clearToasts, forceDarkMode, isFileLoaded]) + }, [fileContents, app, addToast, msg, clearToasts, forceDarkMode, isFileLoaded]) return null } diff --git a/apps/vscode/editor/src/app.tsx b/apps/vscode/editor/src/app.tsx index 8c67abcdb..69f11b628 100644 --- a/apps/vscode/editor/src/app.tsx +++ b/apps/vscode/editor/src/app.tsx @@ -2,19 +2,18 @@ import { App, Canvas, ErrorBoundary, - setRuntimeOverrides, + TAB_ID, TldrawEditor, - TldrawEditorConfig, + setRuntimeOverrides, } from '@tldraw/editor' import { linksUiOverrides } from './utils/links' // eslint-disable-next-line import/no-internal-modules import '@tldraw/editor/editor.css' -import { TAB_ID, useLocalSyncClient } from '@tldraw/tlsync-client' import { ContextMenu, MenuSchema, TldrawUi } from '@tldraw/ui' // eslint-disable-next-line import/no-internal-modules -import { getAssetUrlsByImport } from '@tldraw/assets/imports' -// eslint-disable-next-line import/no-internal-modules import '@tldraw/ui/ui.css' +// eslint-disable-next-line import/no-internal-modules +import { getAssetUrlsByImport } from '@tldraw/assets/imports' import { useEffect, useMemo, useState } from 'react' import { VscodeMessage } from '../../messages' import '../public/index.css' @@ -24,10 +23,6 @@ import { FullPageMessage } from './FullPageMessage' import { onCreateBookmarkFromUrl } from './utils/bookmarks' import { vscode } from './utils/vscode' -const config = new TldrawEditorConfig() - -// @ts-ignore - setRuntimeOverrides({ openWindow: (url, target) => { vscode.postMessage({ @@ -97,7 +92,6 @@ export const TldrawWrapper = () => { fileContents: message.data.fileContents, uri: message.data.uri, isDarkMode: message.data.isDarkMode, - config, }) // We only want to listen for this message once window.removeEventListener('message', handleMessage) @@ -127,32 +121,23 @@ export type TLDrawInnerProps = { fileContents: string uri: string isDarkMode: boolean - config: TldrawEditorConfig } -function TldrawInner({ uri, config, assetSrc, isDarkMode, fileContents }: TLDrawInnerProps) { - const instanceId = TAB_ID - const syncedStore = useLocalSyncClient({ - universalPersistenceKey: uri, - instanceId, - config, - }) - +function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerProps) { const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc]) return ( {/* */} - - + + diff --git a/apps/vscode/editor/tsconfig.json b/apps/vscode/editor/tsconfig.json index 815996187..cb90a5bb6 100644 --- a/apps/vscode/editor/tsconfig.json +++ b/apps/vscode/editor/tsconfig.json @@ -29,7 +29,6 @@ { "path": "../../../packages/file-format" }, { "path": "../../../packages/ui" }, { "path": "../../../packages/editor" }, - { "path": "../../../packages/tlsync-client" }, { "path": "../../../packages/utils" } ] } diff --git a/apps/vscode/extension/package.json b/apps/vscode/extension/package.json index e2aa366c4..6a22cf1cf 100644 --- a/apps/vscode/extension/package.json +++ b/apps/vscode/extension/package.json @@ -124,7 +124,6 @@ "scripts": { "dev": "tsx scripts/dev.ts", "build": "cd ../editor && yarn build && cd ../extension && tsx scripts/build.ts", - "web": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=.", "package": "yarn build && tsx scripts/package.ts", "publish": "vsce publish", "lint": "yarn run -T tsx ../../../scripts/lint.ts", diff --git a/apps/vscode/extension/src/file.ts b/apps/vscode/extension/src/file.ts index eac3650e8..716b8d0f3 100644 --- a/apps/vscode/extension/src/file.ts +++ b/apps/vscode/extension/src/file.ts @@ -1,16 +1,16 @@ -import { TldrawEditorConfig } from '@tldraw/editor' +import { createTLSchema } from '@tldraw/editor' import { TldrawFile } from '@tldraw/file-format' import * as vscode from 'vscode' export const defaultFileContents: TldrawFile = { tldrawFileFormatVersion: 1, - schema: new TldrawEditorConfig().storeSchema.serialize(), + schema: createTLSchema().serialize(), records: [], } export const fileContentWithErrors: TldrawFile = { tldrawFileFormatVersion: 1, - schema: new TldrawEditorConfig().storeSchema.serialize(), + schema: createTLSchema().serialize(), records: [{ typeName: 'shape', id: null } as any], } diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 51fc7df58..a923c6817 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -29,9 +29,8 @@ import { Matrix2d } from '@tldraw/primitives'; import { Matrix2dModel } from '@tldraw/primitives'; import { Migrations } from '@tldraw/tlstore'; import { Polyline2d } from '@tldraw/primitives'; -import * as React_2 from 'react'; -import { default as React_3 } from 'react'; -import { RecordType } from '@tldraw/tlstore'; +import { default as React_2 } from 'react'; +import * as React_3 from 'react'; import { RotateCorner } from '@tldraw/primitives'; import { SelectionCorner } from '@tldraw/primitives'; import { SelectionEdge } from '@tldraw/primitives'; @@ -39,7 +38,6 @@ import { SelectionHandle } from '@tldraw/primitives'; import { SerializedSchema } from '@tldraw/tlstore'; import { Signal } from 'signia'; import { sortByIndex } from '@tldraw/indices'; -import { StoreSchema } from '@tldraw/tlstore'; import { StoreSnapshot } from '@tldraw/tlstore'; import { StrokePoint } from '@tldraw/primitives'; import { TLAlignType } from '@tldraw/tlschema'; @@ -57,7 +55,6 @@ import { TLColorType } from '@tldraw/tlschema'; import { TLCursor } from '@tldraw/tlschema'; import { TLDocument } from '@tldraw/tlschema'; import { TLDrawShape } from '@tldraw/tlschema'; -import { TLDrawShapeSegment } from '@tldraw/tlschema'; import { TLEmbedShape } from '@tldraw/tlschema'; import { TLFontType } from '@tldraw/tlschema'; import { TLFrameShape } from '@tldraw/tlschema'; @@ -88,7 +85,6 @@ import { TLShapeProps } from '@tldraw/tlschema'; import { TLSizeStyle } from '@tldraw/tlschema'; import { TLSizeType } from '@tldraw/tlschema'; import { TLStore } from '@tldraw/tlschema'; -import { TLStoreProps } from '@tldraw/tlschema'; import { TLStyleCollections } from '@tldraw/tlschema'; import { TLStyleType } from '@tldraw/tlschema'; import { TLTextShape } from '@tldraw/tlschema'; @@ -125,7 +121,7 @@ export type AnimationOptions = Partial<{ // @public (undocumented) export class App extends EventEmitter { - constructor({ config, store, getContainer }: AppOptions); + constructor({ store, user, tools, shapes, getContainer, }: AppOptions); addOpenMenu: (id: string) => this; alignShapes(operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top', ids?: TLShapeId[]): this; get allShapesCommonBounds(): Box2d | null; @@ -167,7 +163,6 @@ export class App extends EventEmitter { // @internal protected _clickManager: ClickManager; complete(): this; - readonly config: TldrawEditorConfig; // @internal (undocumented) crash(error: unknown): void; // @internal @@ -551,9 +546,11 @@ export function applyRotationToSnapshotShapes({ delta, app, snapshot, stage, }: // @public (undocumented) export interface AppOptions { - config: TldrawEditorConfig; getContainer: () => HTMLElement; + shapes?: Record; store: TLStore; + tools?: StateNodeConstructor[]; + user?: TLUser; } // @public (undocumented) @@ -569,8 +566,8 @@ export const BOUND_ARROW_OFFSET = 10; export function buildFromV1Document(app: App, document: LegacyTldrawDocument): void; // @public (undocumented) -export const Canvas: React_2.MemoExoticComponent<({ onDropOverride, }: { - onDropOverride?: ((defaultOnDrop: (e: React_2.DragEvent) => Promise) => (e: React_2.DragEvent) => Promise) | undefined; +export const Canvas: React_3.MemoExoticComponent<({ onDropOverride, }: { + onDropOverride?: ((defaultOnDrop: (e: React_3.DragEvent) => Promise) => (e: React_3.DragEvent) => Promise) | undefined; }) => JSX.Element>; // @public (undocumented) @@ -613,6 +610,9 @@ export function createEmbedShapeAtPoint(app: App, url: string, point: Vec2dModel // @public (undocumented) export function createShapesFromFiles(app: App, files: File[], position: VecLike, _ignoreParent?: boolean): Promise; +// @public +export function createTLStore(opts?: StoreOptions): TLStore; + // @public (undocumented) export function dataTransferItemAsString(item: DataTransferItem): Promise; @@ -658,6 +658,12 @@ export function defaultEmptyAs(str: string, dflt: string): string; // @internal (undocumented) export const DefaultErrorFallback: TLErrorFallback; +// @public (undocumented) +export const defaultShapes: Record; + +// @public (undocumented) +export const defaultTools: StateNodeConstructor[]; + // @internal (undocumented) export const DOUBLE_CLICK_DURATION = 450; @@ -685,7 +691,7 @@ export type EmbedResult = { } | undefined; // @public (undocumented) -export class ErrorBoundary extends React_2.Component>, ErrorBoundaryState> { +export class ErrorBoundary extends React_3.Component>, ErrorBoundaryState> { // (undocumented) componentDidCatch(error: unknown): void; // (undocumented) @@ -693,7 +699,7 @@ export class ErrorBoundary extends React_2.Component React_2.ReactNode; + fallback: (error: unknown) => React_3.ReactNode; // (undocumented) onError?: ((error: unknown) => void) | null; } @@ -713,16 +719,6 @@ export function ErrorScreen({ children }: { children: any; }): JSX.Element; -// @public (undocumented) -export interface ErrorSyncedStore { - // (undocumented) - readonly error: Error; - // (undocumented) - readonly status: 'error'; - // (undocumented) - readonly store?: undefined; -} - // @public (undocumented) export const EVENT_NAME_MAP: Record, keyof TLEventHandlers>; @@ -842,6 +838,9 @@ export function getSvgPathFromStrokePoints(points: StrokePoint[], closed?: boole // @public (undocumented) export function getTextBoundingBox(text: SVGTextElement): DOMRect; +// @public (undocumented) +export function getUserPreferences(): TLUserPreferences; + // @public (undocumented) export const getValidHttpURLList: (url: string) => string[] | undefined; @@ -864,6 +863,11 @@ export const GRID_STEPS: { // @internal (undocumented) export const HAND_TOOL_FRICTION = 0.09; +// @public +export function hardReset({ shouldReload }?: { + shouldReload?: boolean | undefined; +}): Promise; + // @public (undocumented) export function hardResetApp(): void; @@ -874,7 +878,7 @@ export const HASH_PATERN_ZOOM_NAMES: Record; export function HTMLContainer({ children, className, ...rest }: HTMLContainerProps): JSX.Element; // @public (undocumented) -export type HTMLContainerProps = React_2.HTMLAttributes; +export type HTMLContainerProps = React_3.HTMLAttributes; // @public (undocumented) export const ICON_SIZES: Record; @@ -882,16 +886,6 @@ export const ICON_SIZES: Record; // @public (undocumented) export const INDENT = " "; -// @public (undocumented) -export interface InitializingSyncedStore { - // (undocumented) - readonly error?: undefined; - // (undocumented) - readonly status: 'loading'; - // (undocumented) - readonly store?: undefined; -} - // @public export function isAnimated(buffer: ArrayBuffer): boolean; @@ -1392,27 +1386,17 @@ export function openWindow(url: string, target?: string): void; // @internal (undocumented) export function OptionalErrorBoundary({ children, fallback, ...props }: Omit & { - fallback: ((error: unknown) => React_2.ReactNode) | null; + fallback: ((error: unknown) => React_3.ReactNode) | null; }): JSX.Element; // @public -export function preventDefault(event: Event | React_3.BaseSyntheticEvent): void; - -// @public (undocumented) -export interface ReadySyncedStore { - // (undocumented) - readonly error?: undefined; - // (undocumented) - readonly status: 'synced'; - // (undocumented) - readonly store: TLStore; -} +export function preventDefault(event: Event | React_2.BaseSyntheticEvent): void; // @public (undocumented) export function refreshPage(): void; // @public (undocumented) -export function releasePointerCapture(element: Element, event: PointerEvent | React_3.PointerEvent): void; +export function releasePointerCapture(element: Element, event: PointerEvent | React_2.PointerEvent): void; // @internal (undocumented) export const REMOVE_SYMBOL: unique symbol; @@ -1455,7 +1439,7 @@ export const runtime: { export function setDefaultEditorAssetUrls(assetUrls: EditorAssetUrls): void; // @public (undocumented) -export function setPointerCapture(element: Element, event: PointerEvent | React_3.PointerEvent): void; +export function setPointerCapture(element: Element, event: PointerEvent | React_2.PointerEvent): void; // @public (undocumented) export function setPropsForNextShape(previousProps: TLInstancePropsForNextShape, newProps: Partial): TLInstancePropsForNextShape; @@ -1463,6 +1447,9 @@ export function setPropsForNextShape(previousProps: TLInstancePropsForNextShape, // @public (undocumented) export function setRuntimeOverrides(input: Partial): void; +// @public (undocumented) +export function setUserPreferences(user: TLUserPreferences): void; + // @public (undocumented) export function snapToGrid(n: number, gridSize: number): number; @@ -1559,6 +1546,30 @@ export interface StateNodeConstructor { styles?: TLStyleType[]; } +// @public (undocumented) +export type StoreWithStatus = { + readonly status: 'error'; + readonly store?: undefined; + readonly error: Error; +} | { + readonly status: 'loading'; + readonly store?: undefined; + readonly error?: undefined; +} | { + readonly status: 'not-synced'; + readonly store: TLStore; + readonly error?: undefined; +} | { + readonly status: 'synced-local'; + readonly store: TLStore; + readonly error?: undefined; +} | { + readonly status: 'synced-remote'; + readonly connectionStatus: 'offline' | 'online'; + readonly store: TLStore; + readonly error?: undefined; +}; + // @public (undocumented) export const STYLES: TLStyleCollections; @@ -1569,10 +1580,10 @@ export const SVG_PADDING = 32; export function SVGContainer({ children, className, ...rest }: SVGContainerProps): JSX.Element; // @public (undocumented) -export type SVGContainerProps = React_2.HTMLAttributes; +export type SVGContainerProps = React_3.HTMLAttributes; // @public (undocumented) -export type SyncedStore = ErrorSyncedStore | InitializingSyncedStore | ReadySyncedStore; +export const TAB_ID: TLInstanceId; // @public (undocumented) export const TEXT_PROPS: { @@ -1696,7 +1707,7 @@ export type TLBoxLike = TLBaseShape (typeof Idle_4 | typeof Pointing_3)[]; + static children: () => (typeof Idle_4 | typeof Pointing_2)[]; // (undocumented) static id: string; // (undocumented) @@ -1793,51 +1804,31 @@ export type TLCompleteEventInfo = { export type TLCopyType = 'jpeg' | 'json' | 'png' | 'svg'; // @public (undocumented) -export function TldrawEditor(props: TldrawEditorProps): JSX.Element; +export const TldrawEditor: React_2.NamedExoticComponent; // @public (undocumented) -export class TldrawEditorConfig { - constructor(opts?: TldrawEditorConfigOptions); - // (undocumented) - createStore(config: { - initialData?: StoreSnapshot; - instanceId: TLInstanceId; - }): TLStore; - // (undocumented) - readonly derivePresenceState: (store: TLStore) => Signal; - // (undocumented) - readonly setUserPreferences: (userPreferences: TLUserPreferences) => void; - // (undocumented) - readonly shapeUtils: Record>; - // (undocumented) - readonly storeSchema: StoreSchema; - // (undocumented) - readonly TLShape: RecordType; - // (undocumented) - readonly tools: readonly StateNodeConstructor[]; - // (undocumented) - readonly userPreferences: Signal; -} - -// @public (undocumented) -export interface TldrawEditorProps { +export type TldrawEditorProps = { + children?: any; + shapes?: Record; + tools?: StateNodeConstructor[]; assetUrls?: EditorAssetUrls; autoFocus?: boolean; - // (undocumented) - children?: any; components?: Partial; - config: TldrawEditorConfig; - instanceId?: TLInstanceId; - isDarkMode?: boolean; + onMount?: (app: App) => void; onCreateAssetFromFile?: (file: File) => Promise; onCreateBookmarkFromUrl?: (url: string) => Promise<{ image: string; title: string; description: string; }>; - onMount?: (app: App) => void; - store?: SyncedStore | TLStore; -} +} & ({ + store: StoreWithStatus | TLStore; +} | { + store?: undefined; + initialData?: StoreSnapshot; + instanceId?: TLInstanceId; + persistenceKey?: string; +}); // @public (undocumented) export class TLDrawUtil extends TLShapeUtil { @@ -2209,6 +2200,8 @@ export class TLGroupUtil extends TLShapeUtil { render(shape: TLGroupShape): JSX.Element | null; // (undocumented) static type: string; + // (undocumented) + type: "group"; } // @public (undocumented) @@ -2570,6 +2563,8 @@ export abstract class TLShapeUtil { export interface TLShapeUtilConstructor = TLShapeUtil> { // (undocumented) new (app: App, type: T['type']): ShapeUtil; + // (undocumented) + type: T['type']; } // @public (undocumented) @@ -2663,6 +2658,22 @@ export class TLTextUtil extends TLShapeUtil { // @public (undocumented) export type TLTickEvent = (elapsed: number) => void; +// @public +export interface TLUserPreferences { + // (undocumented) + animationSpeed: number; + // (undocumented) + color: string; + // (undocumented) + id: string; + // (undocumented) + isDarkMode: boolean; + // (undocumented) + locale: string; + // (undocumented) + name: string; +} + // @public (undocumented) export class TLVideoUtil extends TLBoxUtil { // (undocumented) @@ -2715,6 +2726,11 @@ export const useApp: () => App; // @public (undocumented) export function useContainer(): HTMLDivElement; +// @internal (undocumented) +export function useLocalStore(opts?: { + persistenceKey?: string | undefined; +} & StoreOptions): StoreWithStatus; + // @internal (undocumented) export function usePeerIds(): string[]; @@ -2733,6 +2749,9 @@ export const USER_COLORS: readonly ["#FF802B", "#EC5E41", "#F2555A", "#F04F88", // @public (undocumented) export function useReactor(name: string, reactFn: () => void, deps?: any[] | undefined): void; +// @public (undocumented) +export function useTLStore(opts: StoreOptions): TLStore; + // @internal (undocumented) export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10; diff --git a/packages/editor/package.json b/packages/editor/package.json index 324945361..0c3a8e0d7 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -56,6 +56,7 @@ "crc": "^4.3.2", "escape-string-regexp": "^5.0.0", "eventemitter3": "^4.0.7", + "idb": "^7.1.1", "is-plain-object": "^5.0.0", "lodash.throttle": "^4.1.1", "lodash.uniq": "^4.5.0", @@ -79,7 +80,7 @@ "@types/wicg-file-system-access": "^2020.9.5", "benchmark": "^2.1.4", "fake-indexeddb": "^4.0.0", - "jest-canvas-mock": "^2.4.0", + "jest-canvas-mock": "^2.5.1", "jest-environment-jsdom": "^29.4.3", "lazyrepo": "0.0.0-alpha.26", "react-test-renderer": "^18.2.0", @@ -103,6 +104,7 @@ }, "setupFiles": [ "raf/polyfill", + "jest-canvas-mock", "/setupTests.js" ], "setupFilesAfterEnv": [ diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 6abc65eef..b91acd2ea 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -127,13 +127,14 @@ export { export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer' export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer' export { - type ErrorSyncedStore, - type InitializingSyncedStore, - type ReadySyncedStore, - type SyncedStore, -} from './lib/config/SyncedStore' -export { USER_COLORS } from './lib/config/TLUserPreferences' -export { TldrawEditorConfig } from './lib/config/TldrawEditorConfig' + USER_COLORS, + getUserPreferences, + setUserPreferences, + type TLUserPreferences, +} from './lib/config/TLUserPreferences' +export { createTLStore } from './lib/config/createTLStore' +export { defaultShapes } from './lib/config/defaultShapes' +export { defaultTools } from './lib/config/defaultTools' export { ANIMATION_MEDIUM_MS, ANIMATION_SHORT_MS, @@ -176,10 +177,12 @@ export { normalizeWheel } from './lib/hooks/shared' export { useApp } from './lib/hooks/useApp' export { useContainer } from './lib/hooks/useContainer' export type { TLEditorComponents } from './lib/hooks/useEditorComponents' +export { useLocalStore } from './lib/hooks/useLocalStore' export { usePeerIds } from './lib/hooks/usePeerIds' export { usePresence } from './lib/hooks/usePresence' export { useQuickReactor } from './lib/hooks/useQuickReactor' export { useReactor } from './lib/hooks/useReactor' +export { useTLStore } from './lib/hooks/useTLStore' export { WeakMapCache } from './lib/utils/WeakMapCache' export { ACCEPTED_ASSET_TYPE, @@ -256,4 +259,7 @@ export { defaultEmptyAs, } from './lib/utils/string' export { getPointerInfo, getSvgPathFromStroke, getSvgPathFromStrokePoints } from './lib/utils/svg' +export { type StoreWithStatus } from './lib/utils/sync/StoreWithStatus' +export { hardReset } from './lib/utils/sync/hardReset' +export { TAB_ID } from './lib/utils/sync/persistence-constants' export { openWindow } from './lib/utils/window-open' diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index 2f5c57b61..2f83c7b65 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -1,15 +1,13 @@ -import { InstanceRecordType, TLAsset, TLInstanceId, TLStore } from '@tldraw/tlschema' -import { Store } from '@tldraw/tlstore' +import { TLAsset, TLInstanceId, TLRecord, TLStore } from '@tldraw/tlschema' +import { Store, StoreSnapshot } from '@tldraw/tlstore' import { annotateError } from '@tldraw/utils' -import React, { useCallback, useMemo, useSyncExternalStore } from 'react' +import React, { memo, useCallback, useLayoutEffect, useState, useSyncExternalStore } from 'react' import { App } from './app/App' +import { StateNodeConstructor } from './app/statechart/StateNode' import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls' -import { OptionalErrorBoundary } from './components/ErrorBoundary' - -import { SyncedStore } from './config/SyncedStore' -import { TldrawEditorConfig } from './config/TldrawEditorConfig' - import { DefaultErrorFallback } from './components/DefaultErrorFallback' +import { OptionalErrorBoundary } from './components/ErrorBoundary' +import { ShapeInfo } from './config/createTLStore' import { AppContext } from './hooks/useApp' import { ContainerProvider, useContainer } from './hooks/useContainer' import { useCursor } from './hooks/useCursor' @@ -21,21 +19,38 @@ import { } from './hooks/useEditorComponents' import { useEvent } from './hooks/useEvent' import { useForceUpdate } from './hooks/useForceUpdate' +import { useLocalStore } from './hooks/useLocalStore' import { usePreloadAssets } from './hooks/usePreloadAssets' import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix' import { useZoomCss } from './hooks/useZoomCss' +import { StoreWithStatus } from './utils/sync/StoreWithStatus' +import { TAB_ID } from './utils/sync/persistence-constants' /** @public */ -export interface TldrawEditorProps { +export type TldrawEditorProps = { children?: any - /** A configuration defining major customizations to the app, such as custom shapes and new tools */ - config: TldrawEditorConfig - /** Overrides for the tldraw components */ - components?: Partial - /** 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 + /** + * 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 + /** + * Called when the editor has mounted. * * @example * @@ -49,7 +64,7 @@ export interface TldrawEditorProps { */ onMount?: (app: App) => void /** - * Called when the app generates a new asset from a file, such as when an image is dropped into + * Called when the editor generates a new asset from a file, such as when an image is dropped into * the canvas. * * @example @@ -81,22 +96,31 @@ export interface TldrawEditorProps { onCreateBookmarkFromUrl?: ( url: string ) => Promise<{ image: string; title: string; description: string }> - - /** - * The Store instance to use for keeping the app's data. This may be prepopulated, e.g. by loading - * from a server or database. - */ - store?: TLStore | SyncedStore - /** - * The id of the app instance (e.g. a browser tab if the app will have only one tldraw app per - * tab). If not given, one will be generated. - */ - instanceId?: TLInstanceId - /** Asset URLs */ - assetUrls?: EditorAssetUrls - /** Whether to automatically focus the editor when it mounts. */ - autoFocus?: boolean -} +} & ( + | { + /** + * The Store instance to use for keeping the editor's data. This may be prepopulated, e.g. by loading + * from a server or database. + */ + store: TLStore | StoreWithStatus + } + | { + store?: undefined + /** + * The editor's initial data. + */ + initialData?: StoreSnapshot + /** + * The id of the editor instance (e.g. a browser tab if the editor will have only one tldraw app per + * tab). If not given, one will be generated. + */ + instanceId?: TLInstanceId + /** + * The id under which to sync and persist the editor's data. + */ + persistenceKey?: string + } +) declare global { interface Window { @@ -105,12 +129,15 @@ declare global { } /** @public */ -export function TldrawEditor(props: TldrawEditorProps) { +export const TldrawEditor = memo(function TldrawEditor(props: TldrawEditorProps) { const [container, setContainer] = React.useState(null) - const { components, ...rest } = props const ErrorFallback = - components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback + props.components?.ErrorFallback === undefined + ? DefaultErrorFallback + : props.components?.ErrorFallback + + const { store, ...rest } = props return (
@@ -120,51 +147,68 @@ export function TldrawEditor(props: TldrawEditorProps) { > {container && ( - - + + {store ? ( + store instanceof Store ? ( + // Store is ready to go, whether externally synced or not + + ) : ( + // Store is a synced store, so handle syncing stages internally + + ) + ) : ( + // We have no store (it's undefined) so create one and possibly sync it + + )} )}
) +}) + +function TldrawEditorWithOwnStore(props: TldrawEditorProps & { store: undefined }) { + const { initialData, instanceId = TAB_ID, shapes, persistenceKey } = props + + const syncedStore = useLocalStore({ + customShapes: shapes, + instanceId, + initialData, + persistenceKey, + }) + + return } -function TldrawEditorBeforeLoading({ config, instanceId, store, ...props }: TldrawEditorProps) { +const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({ + store, + assetUrls, + ...rest +}: TldrawEditorProps & { store: StoreWithStatus }) { const { done: preloadingComplete, error: preloadingError } = usePreloadAssets( - props.assetUrls ?? defaultEditorAssetUrls + assetUrls ?? defaultEditorAssetUrls ) - const _store = useMemo(() => { - return ( - store ?? - config.createStore({ - instanceId: instanceId ?? InstanceRecordType.createId(), - }) - ) - }, [store, config, instanceId]) - - let loadedStore: TLStore | SyncedStore - if (!(_store instanceof Store)) { - if (_store.error) { + switch (store.status) { + case 'error': { // for error handling, we fall back to the default error boundary. // if users want to handle this error differently, they can render // their own error screen before the TldrawEditor component - throw _store.error + throw store.error } - if (!_store.store) { + case 'loading': { return Connecting... } - - loadedStore = _store.store - } else { - loadedStore = _store - } - - if (instanceId && loadedStore.props.instanceId !== instanceId) { - console.error( - `The store's instanceId (${loadedStore.props.instanceId}) does not match the instanceId prop (${instanceId}). This may cause unexpected behavior.` - ) + case 'not-synced': { + break + } + case 'synced-local': { + break + } + case 'synced-remote': { + break + } } if (preloadingError) { @@ -175,57 +219,56 @@ function TldrawEditorBeforeLoading({ config, instanceId, store, ...props }: Tldr return Loading assets... } - return -} + return +}) -function TldrawEditorAfterLoading({ +function TldrawEditorWithReadyStore({ onMount, - config, children, onCreateAssetFromFile, onCreateBookmarkFromUrl, store, + tools, + shapes, autoFocus, -}: Omit & { - config: TldrawEditorConfig +}: TldrawEditorProps & { store: TLStore }) { - const container = useContainer() - - const [app, setApp] = React.useState(null) const { ErrorFallback } = useEditorComponents() + const container = useContainer() + const [app, setApp] = useState(null) - React.useLayoutEffect(() => { + useLayoutEffect(() => { const app = new App({ store, - config, + shapes, + tools, getContainer: () => container, }) - setApp(app) - - if (autoFocus) { - app.focus() - } ;(window as any).app = app + setApp(app) return () => { app.dispose() - setApp((prevApp) => (prevApp === app ? null : prevApp)) } - }, [container, config, store, autoFocus]) + }, [container, shapes, tools, store]) React.useEffect(() => { - if (app) { - // Overwrite the default onCreateAssetFromFile handler. - if (onCreateAssetFromFile) { - app.onCreateAssetFromFile = onCreateAssetFromFile - } + if (!app) return - if (onCreateBookmarkFromUrl) { - app.onCreateBookmarkFromUrl = onCreateBookmarkFromUrl - } + // Overwrite the default onCreateAssetFromFile handler. + if (onCreateAssetFromFile) { + app.onCreateAssetFromFile = onCreateAssetFromFile + } + + if (onCreateBookmarkFromUrl) { + app.onCreateBookmarkFromUrl = onCreateBookmarkFromUrl } }, [app, onCreateAssetFromFile, onCreateBookmarkFromUrl]) + React.useLayoutEffect(() => { + if (app && autoFocus) app.focus() + }, [app, autoFocus]) + const onMountEvent = useEvent((app: App) => { onMount?.(app) app.emit('mount') @@ -233,10 +276,7 @@ function TldrawEditorAfterLoading({ }) React.useEffect(() => { - if (app) { - // Run onMount - onMountEvent(app) - } + if (app) onMountEvent(app) }, [app, onMountEvent]) const crashingError = useSyncExternalStore( diff --git a/packages/editor/src/lib/app/App.ts b/packages/editor/src/lib/app/App.ts index aa887f144..a4dab6415 100644 --- a/packages/editor/src/lib/app/App.ts +++ b/packages/editor/src/lib/app/App.ts @@ -64,7 +64,7 @@ import { isShape, isShapeId, } from '@tldraw/tlschema' -import { ComputedCache, HistoryEntry, UnknownRecord } from '@tldraw/tlstore' +import { ComputedCache, HistoryEntry, RecordType, UnknownRecord } from '@tldraw/tlstore' import { annotateError, compact, @@ -77,7 +77,10 @@ import { import { EventEmitter } from 'eventemitter3' import { nanoid } from 'nanoid' import { EMPTY_ARRAY, atom, computed, transact } from 'signia' -import { TldrawEditorConfig } from '../config/TldrawEditorConfig' +import { ShapeInfo } from '../config/createTLStore' +import { TLUser, createTLUser } from '../config/createTLUser' +import { coreShapes, defaultShapes } from '../config/defaultShapes' +import { defaultTools } from '../config/defaultTools' import { ANIMATION_MEDIUM_MS, BLACKLISTED_PROPS, @@ -132,7 +135,7 @@ import { TLResizeMode, TLShapeUtil } from './shapeutils/TLShapeUtil' import { TLTextUtil } from './shapeutils/TLTextUtil/TLTextUtil' import { TLExportColors } from './shapeutils/shared/TLExportColors' import { RootState } from './statechart/RootState' -import { StateNode } from './statechart/StateNode' +import { StateNode, StateNodeConstructor } from './statechart/StateNode' import { TLClipboardModel } from './types/clipboard-types' import { TLEventMap } from './types/emit-types' import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo } from './types/event-types' @@ -161,8 +164,18 @@ export interface AppOptions { * from a server or database. */ store: TLStore - /** A configuration defining major customizations to the app, such as custom shapes and new tools */ - config: TldrawEditorConfig + /** + * An array of shapes to use in the app. These will be used to create and manage shapes in the app. + */ + shapes?: Record + /** + * An array of tools to use in the app. These will be used to handle events and manage user interactions in the app. + */ + tools?: StateNodeConstructor[] + /** + * A user defined externally to replace the default user. + */ + user?: TLUser /** * Should return a containing html element which has all the styles applied to the app. If not * given, the body element will be used. @@ -177,28 +190,54 @@ export function isShapeWithHandles(shape: TLShape) { /** @public */ export class App extends EventEmitter { - constructor({ config, store, getContainer }: AppOptions) { + constructor({ + store, + user, + tools = defaultTools, + shapes = defaultShapes, + getContainer, + }: AppOptions) { super() - this.config = config - - if (store.schema !== this.config.storeSchema) { - throw new Error('Store schema does not match schema given to App') - } - this.store = store - this.user = new UserPreferencesManager(this) + this.user = new UserPreferencesManager(user ?? createTLUser()) this.getContainer = getContainer ?? (() => document.body) this.textMeasure = new TextManager(this) - // Set the shape utils - this.shapeUtils = Object.fromEntries( - Object.entries(this.config.shapeUtils).map(([type, Util]) => [type, new Util(this, type)]) + this.root = new RootState(this) + + // Shapes. + // Accept shapes from constructor parameters which may not conflict with the root note's core tools. + const shapeUtils = Object.fromEntries( + Object.values(coreShapes).map(({ util: Util }) => [Util.type, new Util(this, Util.type)]) ) + for (const [type, { util: Util }] of Object.entries(shapes)) { + if (shapeUtils[type]) { + throw Error(`May not overwrite core shape of type "${type}".`) + } + if (type !== Util.type) { + throw Error(`Shape util's type "${Util.type}" does not match provided type "${type}".`) + } + shapeUtils[type] = new Util(this, Util.type) + } + this.shapeUtils = shapeUtils + + // Tools. + // Accept tools from constructor parameters which may not conflict with the root note's default or + // "baked in" tools, select and zoom. + const uniqueTools = Object.fromEntries(tools.map((Ctor) => [Ctor.id, Ctor])) + for (const [id, Ctor] of Object.entries(uniqueTools)) { + if (this.root.children?.[id]) { + throw Error(`Can't override tool with id "${id}"`) + } + + this.root.children![id] = new Ctor(this) + } + if (typeof window !== 'undefined' && 'navigator' in window) { this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) this.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i) @@ -212,13 +251,6 @@ export class App extends EventEmitter { // Set styles this.colors = new Map(App.styles.color.map((c) => [c.id, `var(--palette-${c.id})`])) - this.root = new RootState(this) - if (this.root.children) { - this.config.tools.forEach((Ctor) => { - this.root.children![Ctor.id] = new Ctor(this) - }) - } - this.store.onBeforeDelete = (record) => { if (record.typeName === 'shape') { this._shapeWillBeDeleted(record) @@ -310,13 +342,6 @@ export class App extends EventEmitter { */ readonly store: TLStore - /** - * The editor's config - * - * @public - */ - readonly config: TldrawEditorConfig - /** * The root state of the statechart. * @@ -4699,7 +4724,12 @@ export class App extends EventEmitter { // When we create the shape, take in the partial (the props coming into the // function) and merge it with the default props. - let shapeRecordToCreate = this.config.TLShape.create({ + let shapeRecordToCreate = ( + this.store.schema.types.shape as RecordType< + TLShape, + 'type' | 'props' | 'index' | 'parentId' + > + ).create({ ...partial, index, parentId: partial.parentId ?? focusLayerId, diff --git a/packages/editor/src/lib/app/managers/UserPreferencesManager.ts b/packages/editor/src/lib/app/managers/UserPreferencesManager.ts index cef9ad9c2..abeb746d9 100644 --- a/packages/editor/src/lib/app/managers/UserPreferencesManager.ts +++ b/packages/editor/src/lib/app/managers/UserPreferencesManager.ts @@ -1,37 +1,37 @@ import { TLUserPreferences } from '../../config/TLUserPreferences' -import { App } from '../App' +import { TLUser } from '../../config/createTLUser' export class UserPreferencesManager { - constructor(private readonly editor: App) {} + constructor(private readonly user: TLUser) {} updateUserPreferences = (userPreferences: Partial) => { - this.editor.config.setUserPreferences({ - ...this.editor.config.userPreferences.value, + this.user.setUserPreferences({ + ...this.user.userPreferences.value, ...userPreferences, }) } get isDarkMode() { - return this.editor.config.userPreferences.value.isDarkMode + return this.user.userPreferences.value.isDarkMode } get animationSpeed() { - return this.editor.config.userPreferences.value.animationSpeed + return this.user.userPreferences.value.animationSpeed } get id() { - return this.editor.config.userPreferences.value.id + return this.user.userPreferences.value.id } get name() { - return this.editor.config.userPreferences.value.name + return this.user.userPreferences.value.name } get locale() { - return this.editor.config.userPreferences.value.locale + return this.user.userPreferences.value.locale } get color() { - return this.editor.config.userPreferences.value.color + return this.user.userPreferences.value.color } } diff --git a/packages/editor/src/lib/app/shapeutils/TLGroupUtil/TLGroupUtil.tsx b/packages/editor/src/lib/app/shapeutils/TLGroupUtil/TLGroupUtil.tsx index d535c9da0..e0d36c1e7 100644 --- a/packages/editor/src/lib/app/shapeutils/TLGroupUtil/TLGroupUtil.tsx +++ b/packages/editor/src/lib/app/shapeutils/TLGroupUtil/TLGroupUtil.tsx @@ -8,6 +8,8 @@ import { DashedOutlineBox } from '../shared/DashedOutlineBox' export class TLGroupUtil extends TLShapeUtil { static override type = 'group' + type = 'group' as const + hideSelectionBoundsBg = () => false hideSelectionBoundsFg = () => true diff --git a/packages/editor/src/lib/app/shapeutils/TLShapeUtil.ts b/packages/editor/src/lib/app/shapeutils/TLShapeUtil.ts index c225e95a8..0ca6adccf 100644 --- a/packages/editor/src/lib/app/shapeutils/TLShapeUtil.ts +++ b/packages/editor/src/lib/app/shapeutils/TLShapeUtil.ts @@ -24,6 +24,7 @@ export interface TLShapeUtilConstructor< ShapeUtil extends TLShapeUtil = TLShapeUtil > { new (app: App, type: T['type']): ShapeUtil + type: T['type'] } /** @public */ diff --git a/packages/editor/src/lib/app/statechart/RootState.ts b/packages/editor/src/lib/app/statechart/RootState.ts index 01bfee06d..e7e4e6570 100644 --- a/packages/editor/src/lib/app/statechart/RootState.ts +++ b/packages/editor/src/lib/app/statechart/RootState.ts @@ -1,37 +1,12 @@ import { TLEventHandlers } from '../types/event-types' import { StateNode } from './StateNode' -import { TLArrowTool } from './TLArrowTool/TLArrowTool' -import { TLDrawTool } from './TLDrawTool/TLDrawTool' -import { TLEraserTool } from './TLEraserTool/TLEraserTool' -import { TLFrameTool } from './TLFrameTool/TLFrameTool' -import { TLGeoTool } from './TLGeoTool/TLGeoTool' -import { TLHandTool } from './TLHandTool/TLHandTool' -import { TLHighlightTool } from './TLHighlightTool/TLHighlightTool' -import { TLLaserTool } from './TLLaserTool/TLLaserTool' -import { TLLineTool } from './TLLineTool/TLLineTool' -import { TLNoteTool } from './TLNoteTool/TLNoteTool' import { TLSelectTool } from './TLSelectTool/TLSelectTool' -import { TLTextTool } from './TLTextTool/TLTextTool' import { TLZoomTool } from './TLZoomTool/TLZoomTool' export class RootState extends StateNode { static override id = 'root' static initial = 'select' - static children = () => [ - TLSelectTool, - TLHandTool, - TLEraserTool, - TLDrawTool, - TLHighlightTool, - TLTextTool, - TLLineTool, - TLArrowTool, - TLGeoTool, - TLNoteTool, - TLFrameTool, - TLZoomTool, - TLLaserTool, - ] + static children = () => [TLSelectTool, TLZoomTool] onKeyDown: TLEventHandlers['onKeyDown'] = (info) => { switch (info.code) { diff --git a/packages/editor/src/lib/app/statechart/TLLaserTool/TLLaserTool.ts b/packages/editor/src/lib/app/statechart/TLLaserTool/TLLaserTool.ts index c64608067..79f86f83e 100644 --- a/packages/editor/src/lib/app/statechart/TLLaserTool/TLLaserTool.ts +++ b/packages/editor/src/lib/app/statechart/TLLaserTool/TLLaserTool.ts @@ -5,6 +5,7 @@ import { Lasering } from './children/Lasering' export class TLLaserTool extends StateNode { static override id = 'laser' + static initial = 'idle' static children = () => [Idle, Lasering] diff --git a/packages/editor/src/lib/config/SyncedStore.tsx b/packages/editor/src/lib/config/SyncedStore.tsx deleted file mode 100644 index 19397dc15..000000000 --- a/packages/editor/src/lib/config/SyncedStore.tsx +++ /dev/null @@ -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 diff --git a/packages/editor/src/lib/config/TLUserPreferences.ts b/packages/editor/src/lib/config/TLUserPreferences.ts index e502c10ef..333a0a4fe 100644 --- a/packages/editor/src/lib/config/TLUserPreferences.ts +++ b/packages/editor/src/lib/config/TLUserPreferences.ts @@ -146,6 +146,7 @@ function storeUserPreferences() { } } +/** @public */ export function setUserPreferences(user: TLUserPreferences) { userTypeValidator.validate(user) globalUserPreferences.set(user) diff --git a/packages/editor/src/lib/config/TldrawEditorConfig.tsx b/packages/editor/src/lib/config/TldrawEditorConfig.tsx deleted file mode 100644 index 1c258fcb8..000000000 --- a/packages/editor/src/lib/config/TldrawEditorConfig.tsx +++ /dev/null @@ -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]: TLShapeUtilConstructor -} = { - 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 - validator?: { validate: (record: T) => T } - migrations?: Migrations - } - > - /** @internal */ - derivePresenceState?: (store: TLStore) => Signal - userPreferences?: Signal - setUserPreferences?: (userPreferences: TLUserPreferences) => void -} - -/** @public */ -export class TldrawEditorConfig { - // Custom tools - readonly tools: readonly StateNodeConstructor[] - - // Custom shape utils - readonly shapeUtils: Record> - - // The record used for TLShape incorporating any custom shapes - readonly TLShape: RecordType - - // The schema used for the store incorporating any custom shapes - readonly storeSchema: StoreSchema - readonly derivePresenceState: (store: TLStore) => Signal - readonly userPreferences: Signal - 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 - instanceId: TLInstanceId - }): TLStore { - let initialData = config.initialData - if (initialData) { - initialData = CLIENT_FIXUP_SCRIPT(initialData) - } - - return new Store({ - schema: this.storeSchema, - initialData, - props: { - instanceId: config?.instanceId ?? InstanceRecordType.createId(), - documentId: TLDOCUMENT_ID, - }, - }) - } -} diff --git a/packages/editor/src/lib/config/createTLStore.ts b/packages/editor/src/lib/config/createTLStore.ts new file mode 100644 index 000000000..c41d6c373 --- /dev/null +++ b/packages/editor/src/lib/config/createTLStore.ts @@ -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 + migrations?: Migrations + validator?: { validate: (record: any) => any } +} + +/** @public */ +export type StoreOptions = { + customShapes?: Record + instanceId?: TLInstanceId + initialData?: StoreSnapshot +} + +/** + * 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, + }, + }) +} diff --git a/packages/editor/src/lib/config/createTLUser.ts b/packages/editor/src/lib/config/createTLUser.ts new file mode 100644 index 000000000..3e78fdd53 --- /dev/null +++ b/packages/editor/src/lib/config/createTLUser.ts @@ -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 + readonly userPreferences: Signal + readonly setUserPreferences: (userPreferences: TLUserPreferences) => void +} + +/** @public */ +export function createTLUser( + opts = {} as { + /** @internal */ + derivePresenceState?: (store: TLStore) => Signal + userPreferences?: Signal + setUserPreferences?: (userPreferences: TLUserPreferences) => void + } +): TLUser { + return { + derivePresenceState: opts.derivePresenceState ?? (() => computed('presence', () => null)), + userPreferences: + opts.userPreferences ?? computed('userPreferences', () => getUserPreferences()), + setUserPreferences: opts.setUserPreferences ?? setUserPreferences, + } +} diff --git a/packages/editor/src/lib/config/defaultShapes.ts b/packages/editor/src/lib/config/defaultShapes.ts new file mode 100644 index 000000000..bb3efa8a8 --- /dev/null +++ b/packages/editor/src/lib/config/defaultShapes.ts @@ -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 = { + // 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 = { + 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, + }, +} diff --git a/packages/editor/src/lib/config/defaultTools.ts b/packages/editor/src/lib/config/defaultTools.ts new file mode 100644 index 000000000..db2b6cb0c --- /dev/null +++ b/packages/editor/src/lib/config/defaultTools.ts @@ -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, +] diff --git a/packages/editor/src/lib/hooks/useCoarsePointer.ts b/packages/editor/src/lib/hooks/useCoarsePointer.ts index 44a24c666..a17a4c1ff 100644 --- a/packages/editor/src/lib/hooks/useCoarsePointer.ts +++ b/packages/editor/src/lib/hooks/useCoarsePointer.ts @@ -4,12 +4,14 @@ import { useApp } from './useApp' export function useCoarsePointer() { const app = useApp() useEffect(() => { - const mql = window.matchMedia('(pointer: coarse)') - const handler = () => { - app.isCoarsePointer = mql.matches + if (window.matchMedia) { + const mql = window.matchMedia('(pointer: coarse)') + const handler = () => { + app.isCoarsePointer = mql.matches + } + handler() + mql.addEventListener('change', handler) + return () => mql.removeEventListener('change', handler) } - handler() - mql.addEventListener('change', handler) - return () => mql.removeEventListener('change', handler) }, [app]) } diff --git a/packages/editor/src/lib/hooks/useLocalStore.ts b/packages/editor/src/lib/hooks/useLocalStore.ts new file mode 100644 index 000000000..a82c27f87 --- /dev/null +++ b/packages/editor/src/lib/hooks/useLocalStore.ts @@ -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' } +} diff --git a/packages/editor/src/lib/hooks/usePattern.tsx b/packages/editor/src/lib/hooks/usePattern.tsx index 967802413..3063bc097 100644 --- a/packages/editor/src/lib/hooks/usePattern.tsx +++ b/packages/editor/src/lib/hooks/usePattern.tsx @@ -14,7 +14,7 @@ const generateImage = (dpr: number, currentZoom: number, darkMode: boolean) => { canvasEl.height = size const ctx = canvasEl.getContext('2d') - if (!ctx) throw new Error('No canvas') + if (!ctx) return ctx.fillStyle = darkMode ? '#212529' : '#f8f9fa' ctx.fillRect(0, 0, size, size) @@ -53,7 +53,9 @@ const canvasBlob = (size: [number, number], fn: (ctx: CanvasRenderingContext2D) const canvas = document.createElement('canvas') canvas.width = size[0] canvas.height = size[1] - fn(canvas.getContext('2d')!) + const ctx = canvas.getContext('2d') + if (!ctx) return '' + fn(ctx) return canvas.toDataURL() } type PatternDef = { zoom: number; url: string; darkMode: boolean } diff --git a/packages/editor/src/lib/hooks/usePrevious.ts b/packages/editor/src/lib/hooks/usePrevious.ts new file mode 100644 index 000000000..3f4f68947 --- /dev/null +++ b/packages/editor/src/lib/hooks/usePrevious.ts @@ -0,0 +1,10 @@ +import { useEffect, useRef } from 'react' + +/** @internal */ +export function usePrevious(value: T) { + const ref = useRef(value) + useEffect(() => { + ref.current = value + }) + return ref.current +} diff --git a/packages/editor/src/lib/hooks/useTLStore.ts b/packages/editor/src/lib/hooks/useTLStore.ts new file mode 100644 index 000000000..4cb515960 --- /dev/null +++ b/packages/editor/src/lib/hooks/useTLStore.ts @@ -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 +} diff --git a/packages/editor/src/lib/test/TestApp.ts b/packages/editor/src/lib/test/TestApp.ts index e8c4f4d59..e32eca857 100644 --- a/packages/editor/src/lib/test/TestApp.ts +++ b/packages/editor/src/lib/test/TestApp.ts @@ -26,7 +26,9 @@ import { TLWheelEventInfo, } from '../app/types/event-types' import { RequiredKeys } from '../app/types/misc-types' -import { TldrawEditorConfig } from '../config/TldrawEditorConfig' +import { createTLStore } from '../config/createTLStore' +import { defaultShapes } from '../config/defaultShapes' +import { defaultTools } from '../config/defaultTools' import { shapesFromJsx } from './jsx' jest.useFakeTimers() @@ -56,12 +58,14 @@ export const TEST_INSTANCE_ID = InstanceRecordType.createCustomId('testInstance1 export class TestApp extends App { constructor(options = {} as Partial>) { const elm = document.createElement('div') + const { shapes = {}, tools = [] } = options elm.tabIndex = 0 - const config = options.config ?? new TldrawEditorConfig() super({ - config, - store: config.createStore({ + shapes: { ...defaultShapes, ...shapes }, + tools: [...defaultTools, ...tools], + store: createTLStore({ instanceId: TEST_INSTANCE_ID, + customShapes: shapes, }), getContainer: () => elm, ...options, diff --git a/packages/editor/src/lib/test/TldrawEditor.test.tsx b/packages/editor/src/lib/test/TldrawEditor.test.tsx index 7437dc1a5..71afbd855 100644 --- a/packages/editor/src/lib/test/TldrawEditor.test.tsx +++ b/packages/editor/src/lib/test/TldrawEditor.test.tsx @@ -1,7 +1,12 @@ -import { render, screen } from '@testing-library/react' -import { InstanceRecordType } from '@tldraw/tlschema' +import { act, render, screen } from '@testing-library/react' +import { InstanceRecordType, TLBaseShape, TLOpacityType } from '@tldraw/tlschema' import { TldrawEditor } from '../TldrawEditor' -import { TldrawEditorConfig } from '../config/TldrawEditorConfig' +import { App } from '../app/App' +import { TLBoxUtil } from '../app/shapeutils/TLBoxUtil' +import { TLBoxTool } from '../app/statechart/TLBoxTool/TLBoxTool' +import { Canvas } from '../components/Canvas' +import { HTMLContainer } from '../components/HTMLContainer' +import { createTLStore } from '../config/createTLStore' let originalFetch: typeof window.fetch beforeEach(() => { @@ -9,7 +14,6 @@ beforeEach(() => { if (args[0] === '/icons/icon/icon-names.json') { return Promise.resolve({ json: () => Promise.resolve([]) } as Response) } - return originalFetch(...args) }) }) @@ -19,43 +23,75 @@ afterEach(() => { window.fetch = originalFetch }) -describe('', () => { - it('Accepts fresh versions of store and calls `onMount` for each one', async () => { - const config = new TldrawEditorConfig() +describe('', () => { + it('Renders without crashing', async () => { + await act(async () => ( + +
+ + )) + }) - const initialStore = config.createStore({ + it('Creates its own store', async () => { + let store: any + render( + await act(async () => ( + (store = app.store)} autoFocus> +
+ + )) + ) + await screen.findByTestId('canvas-1') + expect(store).toBeTruthy() + }) + + it('Renders with an external store', async () => { + const store = createTLStore() + render( + await act(async () => ( + { + expect(app.store).toBe(store) + }} + autoFocus + > +
+ + )) + ) + await screen.findByTestId('canvas-1') + }) + + it('Accepts fresh versions of store and calls `onMount` for each one', async () => { + const initialStore = createTLStore({ instanceId: InstanceRecordType.createCustomId('test'), }) - const onMount = jest.fn() - const rendered = render( - +
) await screen.findByTestId('canvas-1') - expect(onMount).toHaveBeenCalledTimes(1) const initialApp = onMount.mock.lastCall[0] jest.spyOn(initialApp, 'dispose') expect(initialApp.store).toBe(initialStore) - // re-render with the same store: rendered.rerender( - +
) await screen.findByTestId('canvas-2') // not called again: expect(onMount).toHaveBeenCalledTimes(1) - // re-render with a new store: - const newStore = config.createStore({ + const newStore = createTLStore({ instanceId: InstanceRecordType.createCustomId('test'), }) rendered.rerender( - +
) @@ -64,4 +100,188 @@ describe('', () => { expect(onMount).toHaveBeenCalledTimes(2) expect(onMount.mock.lastCall[0].store).toBe(newStore) }) + + it('Renders the canvas and shapes', async () => { + let app = {} as App + render( + await act(async () => ( + { + app = editorApp + }} + > + +
+ + )) + ) + 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 { + 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 ( + + {bounds.w.toFixed()}x{bounds.h.toFixed()} + + ) + } + + indicator(shape: CardShape) { + return + } + } + + 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 () => ( + { + app = editorApp + }} + > + +
+ + )) + ) + 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') + }) }) diff --git a/packages/editor/src/lib/test/tools/translating.test.ts b/packages/editor/src/lib/test/tools/translating.test.ts index 4fc2b1d8b..97510dbe4 100644 --- a/packages/editor/src/lib/test/tools/translating.test.ts +++ b/packages/editor/src/lib/test/tools/translating.test.ts @@ -2,9 +2,9 @@ import { Box2d, Vec2d, VecLike } from '@tldraw/primitives' import { TLShapeId, TLShapePartial, Vec2dModel, createCustomShapeId } from '@tldraw/tlschema' import { GapsSnapLine, PointsSnapLine, SnapLine } from '../../app/managers/SnapManager' import { TLShapeUtil } from '../../app/shapeutils/TLShapeUtil' -import { TldrawEditorConfig } from '../../config/TldrawEditorConfig' import { TestApp } from '../TestApp' +import { defaultShapes } from '../../config/defaultShapes' import { getSnapLines } from '../testutils/getSnapLines' type __TopLeftSnapOnlyShape = any @@ -40,14 +40,6 @@ class __TopLeftSnapOnlyShapeUtil extends TLShapeUtil<__TopLeftSnapOnlyShape> { } } -const configWithCustomShape = new TldrawEditorConfig({ - shapes: { - __test_top_left_snap_only: { - util: __TopLeftSnapOnlyShapeUtil, - }, - }, -}) - let app: TestApp afterEach(() => { @@ -759,8 +751,12 @@ describe('custom snapping points', () => { beforeEach(() => { app?.dispose() app = new TestApp({ - config: configWithCustomShape, - + shapes: { + ...defaultShapes, + __test_top_left_snap_only: { + util: __TopLeftSnapOnlyShapeUtil, + }, + }, // x───────┐ // │ T │ // │ │ diff --git a/packages/editor/src/lib/utils/sync/StoreWithStatus.ts b/packages/editor/src/lib/utils/sync/StoreWithStatus.ts new file mode 100644 index 000000000..676cd7f2f --- /dev/null +++ b/packages/editor/src/lib/utils/sync/StoreWithStatus.ts @@ -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 + } diff --git a/packages/tlsync-client/src/lib/TLLocalSyncClient.test.ts b/packages/editor/src/lib/utils/sync/TLLocalSyncClient.test.ts similarity index 96% rename from packages/tlsync-client/src/lib/TLLocalSyncClient.test.ts rename to packages/editor/src/lib/utils/sync/TLLocalSyncClient.test.ts index 1cccbaaad..933409706 100644 --- a/packages/tlsync-client/src/lib/TLLocalSyncClient.test.ts +++ b/packages/editor/src/lib/utils/sync/TLLocalSyncClient.test.ts @@ -1,12 +1,8 @@ -import { - InstanceRecordType, - PageRecordType, - TldrawEditorConfig, - TLInstanceId, -} from '@tldraw/editor' +import { InstanceRecordType, PageRecordType, TLInstanceId } from '@tldraw/tlschema' import { promiseWithResolve } from '@tldraw/utils' -import * as idb from './indexedDb' +import { createTLStore } from '../../config/createTLStore' import { TLLocalSyncClient } from './TLLocalSyncClient' +import * as idb from './indexedDb' jest.mock('./indexedDb', () => ({ ...jest.requireActual('./indexedDb'), @@ -31,7 +27,7 @@ function testClient( instanceId: TLInstanceId = InstanceRecordType.createCustomId('test'), channel = new BroadcastChannelMock('test') ) { - const store = new TldrawEditorConfig().createStore({ + const store = createTLStore({ instanceId, }) const onLoad = jest.fn(() => { diff --git a/packages/tlsync-client/src/lib/TLLocalSyncClient.ts b/packages/editor/src/lib/utils/sync/TLLocalSyncClient.ts similarity index 99% rename from packages/tlsync-client/src/lib/TLLocalSyncClient.ts rename to packages/editor/src/lib/utils/sync/TLLocalSyncClient.ts index 2218d2139..6bd44b6fc 100644 --- a/packages/tlsync-client/src/lib/TLLocalSyncClient.ts +++ b/packages/editor/src/lib/utils/sync/TLLocalSyncClient.ts @@ -1,4 +1,4 @@ -import { TLInstanceId, TLRecord, TLStore } from '@tldraw/editor' +import { TLInstanceId, TLRecord, TLStore } from '@tldraw/tlschema' import { RecordsDiff, SerializedSchema, compareSchemas, squashRecordDiffs } from '@tldraw/tlstore' import { assert, hasOwnProperty } from '@tldraw/utils' import { transact } from 'signia' diff --git a/packages/tlsync-client/src/lib/alerts.ts b/packages/editor/src/lib/utils/sync/alerts.ts similarity index 100% rename from packages/tlsync-client/src/lib/alerts.ts rename to packages/editor/src/lib/utils/sync/alerts.ts diff --git a/packages/tlsync-client/src/lib/hardReset.ts b/packages/editor/src/lib/utils/sync/hardReset.ts similarity index 100% rename from packages/tlsync-client/src/lib/hardReset.ts rename to packages/editor/src/lib/utils/sync/hardReset.ts diff --git a/packages/tlsync-client/src/lib/indexedDb.ts b/packages/editor/src/lib/utils/sync/indexedDb.ts similarity index 98% rename from packages/tlsync-client/src/lib/indexedDb.ts rename to packages/editor/src/lib/utils/sync/indexedDb.ts index 6f2617a8c..4f1ead6a9 100644 --- a/packages/tlsync-client/src/lib/indexedDb.ts +++ b/packages/editor/src/lib/utils/sync/indexedDb.ts @@ -1,4 +1,4 @@ -import { TLRecord, TLStoreSchema } from '@tldraw/editor' +import { TLRecord, TLStoreSchema } from '@tldraw/tlschema' import { RecordsDiff, SerializedSchema, StoreSnapshot } from '@tldraw/tlstore' import { IDBPDatabase, openDB } from 'idb' import { STORE_PREFIX, addDbName, getAllIndexDbNames } from './persistence-constants' diff --git a/packages/tlsync-client/src/lib/persistence-constants.ts b/packages/editor/src/lib/utils/sync/persistence-constants.ts similarity index 95% rename from packages/tlsync-client/src/lib/persistence-constants.ts rename to packages/editor/src/lib/utils/sync/persistence-constants.ts index ed6ddecbf..623cc719d 100644 --- a/packages/tlsync-client/src/lib/persistence-constants.ts +++ b/packages/editor/src/lib/utils/sync/persistence-constants.ts @@ -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 diff --git a/packages/file-format/api-report.md b/packages/file-format/api-report.md index ee755f685..3d0b607fe 100644 --- a/packages/file-format/api-report.md +++ b/packages/file-format/api-report.md @@ -8,7 +8,6 @@ import { App } from '@tldraw/editor'; import { MigrationFailureReason } from '@tldraw/tlstore'; import { Result } from '@tldraw/utils'; import { SerializedSchema } from '@tldraw/tlstore'; -import { TldrawEditorConfig } from '@tldraw/editor'; import { TLInstanceId } from '@tldraw/editor'; import { TLStore } from '@tldraw/editor'; import { TLTranslationKey } from '@tldraw/ui'; @@ -22,8 +21,8 @@ export function isV1File(data: any): boolean; export function parseAndLoadDocument(app: App, document: string, msg: (id: TLTranslationKey) => string, addToast: ToastsContextType['addToast'], onV1FileLoad?: () => void, forceDarkMode?: boolean): Promise; // @public (undocumented) -export function parseTldrawJsonFile({ config, json, instanceId, }: { - config: TldrawEditorConfig; +export function parseTldrawJsonFile({ json, instanceId, store, }: { + store: TLStore; json: string; instanceId: TLInstanceId; }): Result; diff --git a/packages/file-format/src/lib/file.ts b/packages/file-format/src/lib/file.ts index 225cfcf8f..f4f368abb 100644 --- a/packages/file-format/src/lib/file.ts +++ b/packages/file-format/src/lib/file.ts @@ -1,9 +1,9 @@ import { App, buildFromV1Document, + createTLStore, fileToBase64, TLAsset, - TldrawEditorConfig, TLInstanceId, TLRecord, TLStore, @@ -81,11 +81,11 @@ export type TldrawFileParseError = /** @public */ export function parseTldrawJsonFile({ - config, json, instanceId, + store, }: { - config: TldrawEditorConfig + store: TLStore json: string instanceId: TLInstanceId }): Result { @@ -123,7 +123,7 @@ export function parseTldrawJsonFile({ let migrationResult: MigrationResult> try { const storeSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r as TLRecord])) - migrationResult = config.storeSchema.migrateStoreSnapshot(storeSnapshot, data.schema) + migrationResult = store.schema.migrateStoreSnapshot(storeSnapshot, data.schema) } catch (e) { // junk data in the migration return Result.err({ type: 'invalidRecords', cause: e }) @@ -137,7 +137,12 @@ export function parseTldrawJsonFile({ // we should be able to validate them. if any of the records at this stage // are invalid, we don't open the file try { - return Result.ok(config.createStore({ initialData: migrationResult.value, instanceId })) + return Result.ok( + createTLStore({ + initialData: migrationResult.value, + instanceId, + }) + ) } catch (e) { // junk data in the records (they're not validated yet!) could cause the // migrations to crash. We treat any throw from a migration as an @@ -205,7 +210,7 @@ export async function parseAndLoadDocument( forceDarkMode?: boolean ) { const parseFileResult = parseTldrawJsonFile({ - config: new TldrawEditorConfig(), + store: createTLStore(), json: document, instanceId: app.instanceId, }) diff --git a/packages/file-format/src/test/file.test.ts b/packages/file-format/src/test/file.test.ts index df065d9b7..87af3d03b 100644 --- a/packages/file-format/src/test/file.test.ts +++ b/packages/file-format/src/test/file.test.ts @@ -1,11 +1,11 @@ -import { createCustomShapeId, InstanceRecordType, TldrawEditorConfig } from '@tldraw/editor' +import { createCustomShapeId, createTLStore, InstanceRecordType, TLStore } from '@tldraw/editor' import { MigrationFailureReason, UnknownRecord } from '@tldraw/tlstore' import { assert } from '@tldraw/utils' import { parseTldrawJsonFile as _parseTldrawJsonFile, TldrawFile } from '../lib/file' -const parseTldrawJsonFile = (config: TldrawEditorConfig, json: string) => +const parseTldrawJsonFile = (store: TLStore, json: string) => _parseTldrawJsonFile({ - config, + store, json, instanceId: InstanceRecordType.createCustomId('instance'), }) @@ -16,26 +16,26 @@ function serialize(file: TldrawFile): string { describe('parseTldrawJsonFile', () => { it('returns an error if the file is not json', () => { - const result = parseTldrawJsonFile(new TldrawEditorConfig(), 'not json') + const store = createTLStore() + const result = parseTldrawJsonFile(store, 'not json') assert(!result.ok) expect(result.error.type).toBe('notATldrawFile') }) it("returns an error if the file doesn't look like a tldraw file", () => { - const result = parseTldrawJsonFile( - new TldrawEditorConfig(), - JSON.stringify({ not: 'a tldraw file' }) - ) + const store = createTLStore() + const result = parseTldrawJsonFile(store, JSON.stringify({ not: 'a tldraw file' })) assert(!result.ok) expect(result.error.type).toBe('notATldrawFile') }) it('returns an error if the file version is too old', () => { + const store = createTLStore() const result = parseTldrawJsonFile( - new TldrawEditorConfig(), + store, serialize({ tldrawFileFormatVersion: 0, - schema: new TldrawEditorConfig().storeSchema.serialize(), + schema: store.schema.serialize(), records: [], }) ) @@ -44,11 +44,12 @@ describe('parseTldrawJsonFile', () => { }) it('returns an error if the file version is too new', () => { + const store = createTLStore() const result = parseTldrawJsonFile( - new TldrawEditorConfig(), + store, serialize({ tldrawFileFormatVersion: 100, - schema: new TldrawEditorConfig().storeSchema.serialize(), + schema: store.schema.serialize(), records: [], }) ) @@ -57,10 +58,11 @@ describe('parseTldrawJsonFile', () => { }) it('returns an error if migrations fail', () => { - const serializedSchema = new TldrawEditorConfig().storeSchema.serialize() + const store = createTLStore() + const serializedSchema = store.schema.serialize() serializedSchema.storeVersion = 100 const result = parseTldrawJsonFile( - new TldrawEditorConfig(), + store, serialize({ tldrawFileFormatVersion: 1, schema: serializedSchema, @@ -71,10 +73,11 @@ describe('parseTldrawJsonFile', () => { assert(result.error.type === 'migrationFailed') expect(result.error.reason).toBe(MigrationFailureReason.TargetVersionTooOld) - const serializedSchema2 = new TldrawEditorConfig().storeSchema.serialize() + const store2 = createTLStore() + const serializedSchema2 = store2.schema.serialize() serializedSchema2.recordVersions.shape.version = 100 const result2 = parseTldrawJsonFile( - new TldrawEditorConfig(), + store2, serialize({ tldrawFileFormatVersion: 1, schema: serializedSchema2, @@ -88,11 +91,12 @@ describe('parseTldrawJsonFile', () => { }) it('returns an error if a record is invalid', () => { + const store = createTLStore() const result = parseTldrawJsonFile( - new TldrawEditorConfig(), + store, serialize({ tldrawFileFormatVersion: 1, - schema: new TldrawEditorConfig().storeSchema.serialize(), + schema: store.schema.serialize(), records: [ { typeName: 'shape', @@ -103,19 +107,21 @@ describe('parseTldrawJsonFile', () => { ], }) ) + assert(!result.ok) assert(result.error.type === 'invalidRecords') expect(result.error.cause).toMatchInlineSnapshot( - `[ValidationError: At shape(id = shape:shape, type = geo).rotation: Expected number, got undefined]` + `[ValidationError: At shape(id = shape:shape, type = geo).x: Expected number, got undefined]` ) }) it('returns a store if the file is valid', () => { + const store = createTLStore() const result = parseTldrawJsonFile( - new TldrawEditorConfig(), + store, serialize({ tldrawFileFormatVersion: 1, - schema: new TldrawEditorConfig().storeSchema.serialize(), + schema: store.schema.serialize(), records: [], }) ) diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 90a6c20aa..73bdd9e74 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -5,18 +5,13 @@ ```ts import { TldrawEditorProps } from '@tldraw/editor'; -import { TldrawUiContextProviderProps } from '@tldraw/ui'; +import { TldrawUiProps } from '@tldraw/ui'; // @public (undocumented) -export function Tldraw(props: Omit & TldrawUiContextProviderProps & { - persistenceKey?: string; - hideUi?: boolean; - config?: TldrawEditorProps['config']; -}): JSX.Element; +export function Tldraw(props: TldrawEditorProps & TldrawUiProps): JSX.Element; export * from "@tldraw/editor"; -export * from "@tldraw/tlsync-client"; export * from "@tldraw/ui"; // (No @packageDocumentation comment for this package) diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index cf928e3a9..cdd24f7a7 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -47,7 +47,6 @@ "dependencies": { "@tldraw/editor": "workspace:*", "@tldraw/polyfills": "workspace:*", - "@tldraw/tlsync-client": "workspace:*", "@tldraw/ui": "workspace:*" }, "peerDependencies": { diff --git a/packages/tldraw/src/index.ts b/packages/tldraw/src/index.ts index 0f366bafb..999e55610 100644 --- a/packages/tldraw/src/index.ts +++ b/packages/tldraw/src/index.ts @@ -4,7 +4,5 @@ import '@tldraw/polyfills' // eslint-disable-next-line local/no-export-star export * from '@tldraw/editor' // eslint-disable-next-line local/no-export-star -export * from '@tldraw/tlsync-client' -// eslint-disable-next-line local/no-export-star export * from '@tldraw/ui' export { Tldraw } from './lib/Tldraw' diff --git a/packages/tldraw/src/lib/Tldraw.tsx b/packages/tldraw/src/lib/Tldraw.tsx index ab4c327ff..0ca054a18 100644 --- a/packages/tldraw/src/lib/Tldraw.tsx +++ b/packages/tldraw/src/lib/Tldraw.tsx @@ -1,38 +1,12 @@ -import { Canvas, TldrawEditor, TldrawEditorConfig, TldrawEditorProps } from '@tldraw/editor' -import { DEFAULT_DOCUMENT_NAME, TAB_ID, useLocalSyncClient } from '@tldraw/tlsync-client' -import { ContextMenu, TldrawUi, TldrawUiContextProviderProps } from '@tldraw/ui' -import { useMemo } from 'react' +import { Canvas, TldrawEditor, TldrawEditorProps } from '@tldraw/editor' +import { ContextMenu, TldrawUi, TldrawUiProps } from '@tldraw/ui' /** @public */ -export function Tldraw( - props: Omit & - TldrawUiContextProviderProps & { - /** The key under which to persist this editor's data to local storage. */ - persistenceKey?: string - /** Whether to hide the user interface and only display the canvas. */ - hideUi?: boolean - /** A custom configuration for this Tldraw editor */ - config?: TldrawEditorProps['config'] - } -) { - const { - config, - children, - persistenceKey = DEFAULT_DOCUMENT_NAME, - instanceId = TAB_ID, - ...rest - } = props - - const _config = useMemo(() => config ?? new TldrawEditorConfig(), [config]) - - const syncedStore = useLocalSyncClient({ - instanceId, - config: _config, - universalPersistenceKey: persistenceKey, - }) +export function Tldraw(props: TldrawEditorProps & TldrawUiProps) { + const { children, ...rest } = props return ( - + diff --git a/packages/tldraw/tsconfig.json b/packages/tldraw/tsconfig.json index 85a041e12..66bea2adf 100644 --- a/packages/tldraw/tsconfig.json +++ b/packages/tldraw/tsconfig.json @@ -8,5 +8,5 @@ "noImplicitReturns": false, "rootDir": "src" }, - "references": [{ "path": "../editor" }, { "path": "../tlsync-client" }, { "path": "../ui" }] + "references": [{ "path": "../editor" }, { "path": "../ui" }] } diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index f9f682a01..522a943c7 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -118,8 +118,8 @@ export function createShapeValidator( }>; // @public -export function createTLSchema(opts?: { - customShapes?: { [K in T["type"]]: CustomShapeInfo; } | undefined; +export function createTLSchema(opts?: { + customShapes: Record; }): StoreSchema; // @public (undocumented) @@ -376,7 +376,7 @@ export const groupShapeTypeValidator: T.Validator; export const handleTypeValidator: T.Validator; // @public (undocumented) -export const highlightShapeMigrations: Migrations; +export const highlightShapeTypeMigrations: Migrations; // @public (undocumented) export const highlightShapeTypeValidator: T.Validator; @@ -477,6 +477,14 @@ export const pointerTypeValidator: T.Validator; // @internal (undocumented) export const rootShapeTypeMigrations: Migrations; +// @public (undocumented) +export type SchemaShapeInfo = { + migrations?: Migrations; + validator?: { + validate: (record: any) => any; + }; +}; + // @public (undocumented) export const scribbleTypeValidator: T.Validator; diff --git a/packages/tlschema/src/createTLSchema.ts b/packages/tlschema/src/createTLSchema.ts index 36d660f74..c06a7b2e8 100644 --- a/packages/tlschema/src/createTLSchema.ts +++ b/packages/tlschema/src/createTLSchema.ts @@ -10,7 +10,7 @@ import { InstancePageStateRecordType } from './records/TLInstancePageState' import { InstancePresenceRecordType } from './records/TLInstancePresence' import { PageRecordType } from './records/TLPage' import { PointerRecordType } from './records/TLPointer' -import { TLShape, TLUnknownShape, rootShapeTypeMigrations } from './records/TLShape' +import { TLShape, rootShapeTypeMigrations } from './records/TLShape' import { UserDocumentRecordType } from './records/TLUserDocument' import { storeMigrations } from './schema' import { arrowShapeTypeMigrations, arrowShapeTypeValidator } from './shapes/TLArrowShape' @@ -20,92 +20,122 @@ import { embedShapeTypeMigrations, embedShapeTypeValidator } from './shapes/TLEm import { frameShapeTypeMigrations, frameShapeTypeValidator } from './shapes/TLFrameShape' import { geoShapeTypeMigrations, geoShapeTypeValidator } from './shapes/TLGeoShape' import { groupShapeTypeMigrations, groupShapeTypeValidator } from './shapes/TLGroupShape' -import { highlightShapeMigrations, highlightShapeTypeValidator } from './shapes/TLHighlightShape' +import { + highlightShapeTypeMigrations, + highlightShapeTypeValidator, +} from './shapes/TLHighlightShape' import { imageShapeTypeMigrations, imageShapeTypeValidator } from './shapes/TLImageShape' import { lineShapeTypeMigrations, lineShapeTypeValidator } from './shapes/TLLineShape' import { noteShapeTypeMigrations, noteShapeTypeValidator } from './shapes/TLNoteShape' import { textShapeTypeMigrations, textShapeTypeValidator } from './shapes/TLTextShape' import { videoShapeTypeMigrations, videoShapeTypeValidator } from './shapes/TLVideoShape' -type DefaultShapeInfo = { - validator: T.Validator - migrations: Migrations +/** @public */ +export type SchemaShapeInfo = { + migrations?: Migrations + validator?: { validate: (record: any) => any } } -const DEFAULT_SHAPES: { [K in TLShape['type']]: DefaultShapeInfo> } = - { - arrow: { migrations: arrowShapeTypeMigrations, validator: arrowShapeTypeValidator }, - bookmark: { migrations: bookmarkShapeTypeMigrations, validator: bookmarkShapeTypeValidator }, - draw: { migrations: drawShapeTypeMigrations, validator: drawShapeTypeValidator }, - embed: { migrations: embedShapeTypeMigrations, validator: embedShapeTypeValidator }, - frame: { migrations: frameShapeTypeMigrations, validator: frameShapeTypeValidator }, - geo: { migrations: geoShapeTypeMigrations, validator: geoShapeTypeValidator }, - group: { migrations: groupShapeTypeMigrations, validator: groupShapeTypeValidator }, - image: { migrations: imageShapeTypeMigrations, validator: imageShapeTypeValidator }, - line: { migrations: lineShapeTypeMigrations, validator: lineShapeTypeValidator }, - note: { migrations: noteShapeTypeMigrations, validator: noteShapeTypeValidator }, - text: { migrations: textShapeTypeMigrations, validator: textShapeTypeValidator }, - video: { migrations: videoShapeTypeMigrations, validator: videoShapeTypeValidator }, - highlight: { migrations: highlightShapeMigrations, validator: highlightShapeTypeValidator }, - } +const coreShapes: Record = { + group: { + migrations: groupShapeTypeMigrations, + validator: groupShapeTypeValidator, + }, + bookmark: { + migrations: bookmarkShapeTypeMigrations, + validator: bookmarkShapeTypeValidator, + }, + embed: { + migrations: embedShapeTypeMigrations, + validator: embedShapeTypeValidator, + }, + image: { + migrations: imageShapeTypeMigrations, + validator: imageShapeTypeValidator, + }, + text: { + migrations: textShapeTypeMigrations, + validator: textShapeTypeValidator, + }, + video: { + migrations: videoShapeTypeMigrations, + validator: videoShapeTypeValidator, + }, +} -type CustomShapeInfo = { - validator?: { validate: (record: T) => T } - migrations?: Migrations +const defaultShapes: Record = { + arrow: { + migrations: arrowShapeTypeMigrations, + validator: arrowShapeTypeValidator, + }, + draw: { + migrations: drawShapeTypeMigrations, + validator: drawShapeTypeValidator, + }, + frame: { + migrations: frameShapeTypeMigrations, + validator: frameShapeTypeValidator, + }, + geo: { + migrations: geoShapeTypeMigrations, + validator: geoShapeTypeValidator, + }, + line: { + migrations: lineShapeTypeMigrations, + validator: lineShapeTypeValidator, + }, + note: { + migrations: noteShapeTypeMigrations, + validator: noteShapeTypeValidator, + }, + highlight: { + migrations: highlightShapeTypeMigrations, + validator: highlightShapeTypeValidator, + }, } /** - * Create a store schema for a tldraw store that includes all the default shapes together with any custom shapes. - * @public */ -export function createTLSchema( + * Create a TLSchema with custom shapes. Custom shapes cannot override default shapes. + * + * @param opts - Options + * + * @public */ +export function createTLSchema( opts = {} as { - customShapes?: { [K in T['type']]: CustomShapeInfo } + customShapes: Record } ) { - const { customShapes = {} } = opts + const { customShapes } = opts - const defaultShapeSubTypeEntries = Object.entries(DEFAULT_SHAPES) as [ - TLShape['type'], - DefaultShapeInfo - ][] - - const customShapeSubTypeEntries = Object.entries(customShapes) as [ - T['type'], - CustomShapeInfo - ][] - - // Create a shape record that incorporates the default shapes and any custom shapes - // into its subtype migrations and validators, so that we can migrate any new custom - // subtypes. Note that migrations AND validators for custom shapes are optional. If - // not provided, we use an empty migrations set and/or an "any" validator. - - const shapeSubTypeMigrationsWithCustomSubTypeMigrations = { - ...Object.fromEntries(defaultShapeSubTypeEntries.map(([k, v]) => [k, v.migrations])), - ...Object.fromEntries( - customShapeSubTypeEntries.map(([k, v]) => [k, v.migrations ?? defineMigrations({})]) - ), + for (const key in customShapes) { + if (key in coreShapes) { + throw Error(`Can't override default shape ${key}!`) + } } - const validatorWithCustomShapeValidators = T.model( - 'shape', - T.union('type', { - ...Object.fromEntries(defaultShapeSubTypeEntries.map(([k, v]) => [k, v.validator])), - ...Object.fromEntries( - customShapeSubTypeEntries.map(([k, v]) => [k, (v.validator as T.Validator) ?? T.any]) - ), - }) - ) + const allShapeEntries = Object.entries({ ...coreShapes, ...defaultShapes, ...customShapes }) - const shapeRecord = createRecordType('shape', { + const ShapeRecordType = createRecordType('shape', { migrations: defineMigrations({ currentVersion: rootShapeTypeMigrations.currentVersion, firstVersion: rootShapeTypeMigrations.firstVersion, migrators: rootShapeTypeMigrations.migrators, subTypeKey: 'type', - subTypeMigrations: shapeSubTypeMigrationsWithCustomSubTypeMigrations, + subTypeMigrations: { + ...Object.fromEntries( + allShapeEntries.map(([k, v]) => [k, v.migrations ?? defineMigrations({})]) + ), + }, }), - validator: validatorWithCustomShapeValidators, scope: 'document', + validator: T.model( + 'shape', + T.union('type', { + ...Object.fromEntries( + allShapeEntries.map(([k, v]) => [k, (v.validator as T.Validator) ?? T.any]) + ), + }) + ), }).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false })) return StoreSchema.create( @@ -116,7 +146,7 @@ export function createTLSchema( instance: InstanceRecordType, instance_page_state: InstancePageStateRecordType, page: PageRecordType, - shape: shapeRecord, + shape: ShapeRecordType, user_document: UserDocumentRecordType, instance_presence: InstancePresenceRecordType, pointer: PointerRecordType, diff --git a/packages/tlschema/src/index.ts b/packages/tlschema/src/index.ts index d9dedcf63..d9e013622 100644 --- a/packages/tlschema/src/index.ts +++ b/packages/tlschema/src/index.ts @@ -24,7 +24,7 @@ export { } from './assets/TLVideoAsset' export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation' export { createPresenceStateDerivation } from './createPresenceStateDerivation' -export { createTLSchema } from './createTLSchema' +export { createTLSchema, type SchemaShapeInfo } from './createTLSchema' export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup' export { type Box2dModel, type Vec2dModel } from './geometry-types' export { @@ -157,7 +157,7 @@ export { type TLGroupShapeProps, } from './shapes/TLGroupShape' export { - highlightShapeMigrations, + highlightShapeTypeMigrations, highlightShapeTypeValidator, type TLHighlightShape, type TLHighlightShapeProps, diff --git a/packages/tlschema/src/shapes/TLHighlightShape.ts b/packages/tlschema/src/shapes/TLHighlightShape.ts index 737114656..62e56418d 100644 --- a/packages/tlschema/src/shapes/TLHighlightShape.ts +++ b/packages/tlschema/src/shapes/TLHighlightShape.ts @@ -18,7 +18,6 @@ export type TLHighlightShapeProps = { /** @public */ export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps> -// --- VALIDATION --- /** @public */ export const highlightShapeTypeValidator: T.Validator = createShapeValidator( 'highlight', @@ -32,6 +31,5 @@ export const highlightShapeTypeValidator: T.Validator = create }) ) -// --- MIGRATIONS --- /** @public */ -export const highlightShapeMigrations = defineMigrations({}) +export const highlightShapeTypeMigrations = defineMigrations({}) diff --git a/packages/tlsync-client/CHANGELOG.md b/packages/tlsync-client/CHANGELOG.md deleted file mode 100644 index 6b92f8b33..000000000 --- a/packages/tlsync-client/CHANGELOG.md +++ /dev/null @@ -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 diff --git a/packages/tlsync-client/LICENSE b/packages/tlsync-client/LICENSE deleted file mode 100644 index 4f227c380..000000000 --- a/packages/tlsync-client/LICENSE +++ /dev/null @@ -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. diff --git a/packages/tlsync-client/README.md b/packages/tlsync-client/README.md deleted file mode 100644 index bb4f6cb2e..000000000 --- a/packages/tlsync-client/README.md +++ /dev/null @@ -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). diff --git a/packages/tlsync-client/api-extractor.json b/packages/tlsync-client/api-extractor.json deleted file mode 100644 index f1ed80e93..000000000 --- a/packages/tlsync-client/api-extractor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../config/api-extractor.json" -} diff --git a/packages/tlsync-client/api-report.md b/packages/tlsync-client/api-report.md deleted file mode 100644 index 36da2194c..000000000 --- a/packages/tlsync-client/api-report.md +++ /dev/null @@ -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; - -// @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) - -``` diff --git a/packages/tlsync-client/package.json b/packages/tlsync-client/package.json deleted file mode 100644 index 579a6ec55..000000000 --- a/packages/tlsync-client/package.json +++ /dev/null @@ -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": { - "^~(.*)": "/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" - } -} diff --git a/packages/tlsync-client/setupJest.js b/packages/tlsync-client/setupJest.js deleted file mode 100644 index 83a93db34..000000000 --- a/packages/tlsync-client/setupJest.js +++ /dev/null @@ -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 - }, -} diff --git a/packages/tlsync-client/src/index.ts b/packages/tlsync-client/src/index.ts deleted file mode 100644 index c414107c9..000000000 --- a/packages/tlsync-client/src/index.ts +++ /dev/null @@ -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' diff --git a/packages/tlsync-client/src/lib/hooks/useLocalSyncClient.ts b/packages/tlsync-client/src/lib/hooks/useLocalSyncClient.ts deleted file mode 100644 index c5f094fe0..000000000 --- a/packages/tlsync-client/src/lib/hooks/useLocalSyncClient.ts +++ /dev/null @@ -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' } -} diff --git a/packages/tlsync-client/tsconfig.json b/packages/tlsync-client/tsconfig.json deleted file mode 100644 index 3604fadf0..000000000 --- a/packages/tlsync-client/tsconfig.json +++ /dev/null @@ -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" }] -} diff --git a/packages/ui/api-report.md b/packages/ui/api-report.md index 098d2bada..a436a9bd7 100644 --- a/packages/ui/api-report.md +++ b/packages/ui/api-report.md @@ -621,10 +621,10 @@ export interface TLDialog { // @public (undocumented) export const TldrawUi: React_2.NamedExoticComponent<{ - shareZone?: ReactNode; - renderDebugMenuItems?: (() => React_2.ReactNode) | undefined; children?: ReactNode; hideUi?: boolean | undefined; + shareZone?: ReactNode; + renderDebugMenuItems?: (() => React_2.ReactNode) | undefined; } & TldrawUiContextProviderProps>; // @public (undocumented) @@ -667,6 +667,14 @@ export interface TldrawUiOverrides { translations?: TranslationProviderProps['overrides']; } +// @public (undocumented) +export type TldrawUiProps = { + children?: ReactNode; + hideUi?: boolean; + shareZone?: ReactNode; + renderDebugMenuItems?: () => React_2.ReactNode; +} & TldrawUiContextProviderProps; + // @public (undocumented) export type TLListedTranslation = { readonly locale: string; diff --git a/packages/ui/package.json b/packages/ui/package.json index efb3110d2..7bb92608f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -55,7 +55,6 @@ "@tldraw/editor": "workspace:*", "@tldraw/primitives": "workspace:*", "@tldraw/tlschema": "workspace:*", - "@tldraw/tlsync-client": "workspace:*", "@tldraw/utils": "workspace:*", "browser-fs-access": "^0.31.0", "classnames": "^2.3.2", diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index e8319f8fc..71b4c6ea4 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,7 +1,7 @@ import * as Dialog from './lib/components/primitives/Dialog' import * as DropdownMenu from './lib/components/primitives/DropdownMenu' -export { TldrawUi, TldrawUiContent } from './lib/TldrawUi' +export { TldrawUi, TldrawUiContent, type TldrawUiProps } from './lib/TldrawUi' export { TldrawUiContextProvider, type TldrawUiContextProviderProps, diff --git a/packages/ui/src/lib/TldrawUi.tsx b/packages/ui/src/lib/TldrawUi.tsx index c0f453ce8..ae4b0500d 100644 --- a/packages/ui/src/lib/TldrawUi.tsx +++ b/packages/ui/src/lib/TldrawUi.tsx @@ -24,6 +24,17 @@ import { useNativeClipboardEvents } from './hooks/useClipboardEvents' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' import { useTranslation } from './hooks/useTranslation/useTranslation' +/** @public */ +export type TldrawUiProps = { + children?: ReactNode + /** Whether to hide the interface and only display the canvas. */ + hideUi?: boolean + /** A component to use for the share zone (will be deprecated) */ + shareZone?: ReactNode + /** Additional items to add to the debug menu (will be deprecated)*/ + renderDebugMenuItems?: () => React.ReactNode +} & TldrawUiContextProviderProps + /** * @public */ @@ -33,13 +44,7 @@ export const TldrawUi = React.memo(function TldrawUi({ children, hideUi, ...rest -}: { - shareZone?: ReactNode - renderDebugMenuItems?: () => React.ReactNode - children?: ReactNode - /** Whether to hide the interface and only display the canvas. */ - hideUi?: boolean -} & TldrawUiContextProviderProps) { +}: TldrawUiProps) { return ( Loading assets... - // } - // The hideUi prop should prevent the UI from mounting. // If we ever need want the UI to mount and preserve state, then // we should change this behavior and hide the UI via CSS instead. diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index f5a963eef..587dc0a83 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -8,10 +8,5 @@ "noImplicitReturns": false, "rootDir": "src" }, - "references": [ - { "path": "../editor" }, - { "path": "../primitives" }, - { "path": "../tlsync-client" }, - { "path": "../utils" } - ] + "references": [{ "path": "../editor" }, { "path": "../primitives" }, { "path": "../utils" }] } diff --git a/public-yarn.lock b/public-yarn.lock index c93046e70..9a4919692 100644 --- a/public-yarn.lock +++ b/public-yarn.lock @@ -4297,7 +4297,6 @@ __metadata: "@tldraw/tldraw": "workspace:*" "@tldraw/tlschema": "workspace:*" "@tldraw/tlstore": "workspace:*" - "@tldraw/tlsync-client": "workspace:*" "@tldraw/tlvalidate": "workspace:*" "@tldraw/ui": "workspace:*" "@tldraw/utils": "workspace:*" @@ -4351,8 +4350,9 @@ __metadata: escape-string-regexp: ^5.0.0 eventemitter3: ^4.0.7 fake-indexeddb: ^4.0.0 + idb: ^7.1.1 is-plain-object: ^5.0.0 - jest-canvas-mock: ^2.4.0 + jest-canvas-mock: ^2.5.1 jest-environment-jsdom: ^29.4.3 lazyrepo: 0.0.0-alpha.26 lodash.throttle: ^4.1.1 @@ -4477,7 +4477,6 @@ __metadata: "@testing-library/react": ^14.0.0 "@tldraw/editor": "workspace:*" "@tldraw/polyfills": "workspace:*" - "@tldraw/tlsync-client": "workspace:*" "@tldraw/ui": "workspace:*" chokidar-cli: ^3.0.0 jest-canvas-mock: ^2.4.0 @@ -4521,28 +4520,6 @@ __metadata: languageName: unknown linkType: soft -"@tldraw/tlsync-client@workspace:*, @tldraw/tlsync-client@workspace:packages/tlsync-client": - version: 0.0.0-use.local - resolution: "@tldraw/tlsync-client@workspace:packages/tlsync-client" - dependencies: - "@tldraw/editor": "workspace:*" - "@tldraw/tlstore": "workspace:*" - "@tldraw/utils": "workspace:*" - "@types/react": "*" - "@types/react-dom": "*" - idb: ^7.1.0 - lazyrepo: 0.0.0-alpha.26 - react: "*" - ws: ^8.10.0 - peerDependencies: - signia: "*" - signia-react: "*" - dependenciesMeta: - react: - optional: true - languageName: unknown - linkType: soft - "@tldraw/tlvalidate@workspace:*, @tldraw/tlvalidate@workspace:packages/tlvalidate": version: 0.0.0-use.local resolution: "@tldraw/tlvalidate@workspace:packages/tlvalidate" @@ -4570,7 +4547,6 @@ __metadata: "@tldraw/editor": "workspace:*" "@tldraw/primitives": "workspace:*" "@tldraw/tlschema": "workspace:*" - "@tldraw/tlsync-client": "workspace:*" "@tldraw/utils": "workspace:*" "@types/lz-string": ^1.3.34 browser-fs-access: ^0.31.0 @@ -4606,7 +4582,6 @@ __metadata: "@tldraw/editor": "workspace:*" "@tldraw/file-format": "workspace:*" "@tldraw/tldraw": "workspace:*" - "@tldraw/tlsync-client": "workspace:*" "@tldraw/ui": "workspace:*" "@tldraw/utils": "workspace:*" "@types/fs-extra": ^11.0.1 @@ -5149,15 +5124,6 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:*, @types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.0.6": - version: 18.0.11 - resolution: "@types/react-dom@npm:18.0.11" - dependencies: - "@types/react": "*" - checksum: 579691e4d5ec09688087568037c35edf8cfb1ab3e07f6c60029280733ee7b5c06d66df6fcc90786702c93ac8cb13bc7ff16c79ddfc75d082938fbaa36e1cdbf4 - languageName: node - linkType: hard - "@types/react-dom@npm:<18.0.0": version: 17.0.19 resolution: "@types/react-dom@npm:17.0.19" @@ -5167,6 +5133,15 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.0.6": + version: 18.0.11 + resolution: "@types/react-dom@npm:18.0.11" + dependencies: + "@types/react": "*" + checksum: 579691e4d5ec09688087568037c35edf8cfb1ab3e07f6c60029280733ee7b5c06d66df6fcc90786702c93ac8cb13bc7ff16c79ddfc75d082938fbaa36e1cdbf4 + languageName: node + linkType: hard + "@types/react-router-dom@npm:^5.1.8": version: 5.3.3 resolution: "@types/react-router-dom@npm:5.3.3" @@ -10588,7 +10563,7 @@ __metadata: languageName: node linkType: hard -"idb@npm:^7.1.0, idb@npm:^7.1.1": +"idb@npm:^7.1.1": version: 7.1.1 resolution: "idb@npm:7.1.1" checksum: 1973c28d53c784b177bdef9f527ec89ec239ec7cf5fcbd987dae75a16c03f5b7dfcc8c6d3285716fd0309dd57739805390bd9f98ce23b1b7d8849a3b52de8d56 @@ -11316,6 +11291,16 @@ __metadata: languageName: node linkType: hard +"jest-canvas-mock@npm:^2.5.1": + version: 2.5.1 + resolution: "jest-canvas-mock@npm:2.5.1" + dependencies: + cssfontparser: ^1.2.1 + moo-color: ^1.0.2 + checksum: b8ff56c1b7b7feb6d33b7914dbfac21f19a5a33db0bc092f0426e500e80e67df1286bf817eb780e378b648c9130d7b8ca20cd46e45520657996273a948a7c198 + languageName: node + linkType: hard + "jest-changed-files@npm:^28.1.3": version: 28.1.3 resolution: "jest-changed-files@npm:28.1.3" @@ -15392,7 +15377,7 @@ __metadata: languageName: node linkType: hard -"react@npm:*, react@npm:18.2.0, react@npm:^18.2.0": +"react@npm:18.2.0, react@npm:^18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" dependencies: @@ -18357,9 +18342,9 @@ __metadata: linkType: hard "which-module@npm:^2.0.0": - version: 2.0.1 - resolution: "which-module@npm:2.0.1" - checksum: 1967b7ce17a2485544a4fdd9063599f0f773959cca24176dbe8f405e55472d748b7c549cd7920ff6abb8f1ab7db0b0f1b36de1a21c57a8ff741f4f1e792c52be + version: 2.0.0 + resolution: "which-module@npm:2.0.0" + checksum: 809f7fd3dfcb2cdbe0180b60d68100c88785084f8f9492b0998c051d7a8efe56784492609d3f09ac161635b78ea29219eb1418a98c15ce87d085bce905705c9c languageName: node linkType: hard @@ -18476,7 +18461,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.10.0, ws@npm:^8.11.0, ws@npm:^8.2.3": +"ws@npm:^8.11.0, ws@npm:^8.2.3": version: 8.13.0 resolution: "ws@npm:8.13.0" peerDependencies: