From 0c4174c0b8b0ef1250cc3bd4c4030f99e5204929 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 1 Jun 2023 16:47:34 +0100 Subject: [PATCH] [refactor] User-facing APIs (#1478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates our user-facing APIs for the Tldraw and TldrawEditor components, as well as the Editor (App). It mainly incorporates surface changes from #1450 without any changes to validators or migrators, incorporating feedback / discussion with @SomeHats and @ds300. Here we: - remove the TldrawEditorConfig - bring back a loose version of shape definitions - make a separation between "core" shapes and "default" shapes - do not allow custom shapes, migrators or validators to overwrite core shapes - but _do_ allow new shapes ## `` component In this PR, the `Tldraw` component wraps both the `TldrawEditor` component and our `TldrawUi` component. It accepts a union of props for both components. Previously, this component also added local syncing via a `useLocalSyncClient` hook call, however that has been pushed down to the `TldrawEditor` component. ## `` component The `TldrawEditor` component now more neatly wraps up the different ways that the editor can be configured. ## The store prop (`TldrawEditorProps.store`) There are three main ways for the `TldrawEditor` component to be run: 1. with an externally defined store 2. with an externally defined syncing store (local or remote) 3. with an internally defined store 4. with an internally defined locally syncing store The `store` prop allows for these configurations. If the `store` prop is defined, it may be defined either as a `TLStore` or as a `SyncedStore`. If the store is a `TLStore`, then the Editor will assume that the store is ready to go; if it is defined as a SyncedStore, then the component will display the loading / error screens as needed, or the final editor once the store's status is "synced". When the store is left undefined, then the `TldrawEditor` will create its own internal store using the optional `instanceId`, `initialData`, or `shapes` props to define the store / store schema. If the `persistenceKey` prop is left undefined, then the store will not be synced. If the `persistenceKey` is defined, then the store will be synced locally. In the future, we may also here accept the API key / roomId / etc for creating a remotely synced store. The `SyncedStore` type has been expanded to also include types used for remote syncing, e.g. with `ConnectionStatus`. ## Tools By default, the App has two "baked-in" tools: the select tool and the zoom tool. These cannot (for now) be replaced or removed. The default tools are used by default, but may be replaced by other tools if provided. ## Shapes By default, the App has a set of "core" shapes: - group - embed - bookmark - image - video - text That cannot by overwritten because they're created by the app at different moments, such as when double clicking on the canvas or via a copy and paste event. In follow up PRs, we'll split these out so that users can replace parts of the code where these shapes are created. ### Change Type - [x] `major` — Breaking Change ### Test Plan - [x] Unit Tests --- apps/docs/content/docs/persistence.mdx | 2 +- apps/docs/package.json | 1 - apps/examples/e2e/shared-e2e.ts | 2 +- apps/examples/e2e/tests/test-routes.spec.ts | 73 +++++ .../StoreEventsExample.tsx | 0 .../src/14-persistence/PersistenceExample.tsx | 67 ++--- apps/examples/src/2-api/APIExample.tsx | 2 +- .../examples/src/3-custom-config/CardShape.ts | 10 + apps/examples/src/3-custom-config/CardTool.ts | 13 + .../examples/src/3-custom-config/CardUtil.tsx | 46 ++++ .../3-custom-config/CustomConfigExample.tsx | 123 +-------- .../src/4-custom-ui/CustomUiExample.tsx | 8 +- .../src/5-exploded/ExplodedExample.tsx | 25 +- .../src/7-multiple/MultipleExample.tsx | 2 +- .../ErrorBoundaryExample.tsx | 64 ----- .../8-error-boundary/ErrorBoundaryExample.tsx | 40 +++ .../src/8-error-boundary/ErrorShape.ts | 3 + .../src/8-error-boundary/ErrorUtil.ts | 17 ++ .../{ForEndToEndTests.tsx => end-to-end.tsx} | 3 +- apps/examples/src/index.tsx | 15 +- apps/vscode/editor/package.json | 1 - apps/vscode/editor/src/ChangeResponder.tsx | 28 +- apps/vscode/editor/src/FileOpen.tsx | 6 +- apps/vscode/editor/src/app.tsx | 31 +-- apps/vscode/editor/tsconfig.json | 1 - apps/vscode/extension/package.json | 1 - apps/vscode/extension/src/file.ts | 6 +- packages/editor/api-report.md | 193 +++++++------ packages/editor/package.json | 4 +- packages/editor/src/index.ts | 20 +- packages/editor/src/lib/TldrawEditor.tsx | 228 +++++++++------- packages/editor/src/lib/app/App.ts | 92 ++++--- .../app/managers/UserPreferencesManager.ts | 20 +- .../shapeutils/TLGroupUtil/TLGroupUtil.tsx | 2 + .../src/lib/app/shapeutils/TLShapeUtil.ts | 1 + .../src/lib/app/statechart/RootState.ts | 27 +- .../app/statechart/TLLaserTool/TLLaserTool.ts | 1 + .../editor/src/lib/config/SyncedStore.tsx | 25 -- .../src/lib/config/TLUserPreferences.ts | 1 + .../src/lib/config/TldrawEditorConfig.tsx | 131 --------- .../editor/src/lib/config/createTLStore.ts | 43 +++ .../editor/src/lib/config/createTLUser.ts | 27 ++ .../editor/src/lib/config/defaultShapes.ts | 121 +++++++++ .../editor/src/lib/config/defaultTools.ts | 27 ++ .../editor/src/lib/hooks/useCoarsePointer.ts | 14 +- .../editor/src/lib/hooks/useLocalStore.ts | 59 ++++ packages/editor/src/lib/hooks/usePattern.tsx | 6 +- packages/editor/src/lib/hooks/usePrevious.ts | 10 + packages/editor/src/lib/hooks/useTLStore.ts | 19 ++ packages/editor/src/lib/test/TestApp.ts | 12 +- .../editor/src/lib/test/TldrawEditor.test.tsx | 254 ++++++++++++++++-- .../src/lib/test/tools/translating.test.ts | 18 +- .../src/lib/utils/sync/StoreWithStatus.ts | 30 +++ .../lib/utils/sync}/TLLocalSyncClient.test.ts | 12 +- .../src/lib/utils/sync}/TLLocalSyncClient.ts | 2 +- .../src/lib/utils/sync}/alerts.ts | 0 .../src/lib/utils/sync}/hardReset.ts | 0 .../src/lib/utils/sync}/indexedDb.ts | 2 +- .../lib/utils/sync}/persistence-constants.ts | 3 +- packages/file-format/api-report.md | 5 +- packages/file-format/src/lib/file.ts | 17 +- packages/file-format/src/test/file.test.ts | 48 ++-- packages/tldraw/api-report.md | 9 +- packages/tldraw/package.json | 1 - packages/tldraw/src/index.ts | 2 - packages/tldraw/src/lib/Tldraw.tsx | 36 +-- packages/tldraw/tsconfig.json | 2 +- packages/tlschema/api-report.md | 14 +- packages/tlschema/src/createTLSchema.ts | 154 ++++++----- packages/tlschema/src/index.ts | 4 +- .../tlschema/src/shapes/TLHighlightShape.ts | 4 +- packages/tlsync-client/CHANGELOG.md | 200 -------------- packages/tlsync-client/LICENSE | 190 ------------- packages/tlsync-client/README.md | 5 - packages/tlsync-client/api-extractor.json | 4 - packages/tlsync-client/api-report.md | 34 --- packages/tlsync-client/package.json | 75 ------ packages/tlsync-client/setupJest.js | 10 - packages/tlsync-client/src/index.ts | 3 - .../src/lib/hooks/useLocalSyncClient.ts | 56 ---- packages/tlsync-client/tsconfig.json | 10 - packages/ui/api-report.md | 12 +- packages/ui/package.json | 1 - packages/ui/src/index.ts | 2 +- packages/ui/src/lib/TldrawUi.tsx | 25 +- packages/ui/tsconfig.json | 7 +- public-yarn.lock | 69 ++--- 87 files changed, 1430 insertions(+), 1563 deletions(-) create mode 100644 apps/examples/e2e/tests/test-routes.spec.ts rename apps/examples/src/{13-store => 13-store-events}/StoreEventsExample.tsx (100%) create mode 100644 apps/examples/src/3-custom-config/CardShape.ts create mode 100644 apps/examples/src/3-custom-config/CardTool.ts create mode 100644 apps/examples/src/3-custom-config/CardUtil.tsx delete mode 100644 apps/examples/src/8-error-boundaries/ErrorBoundaryExample.tsx create mode 100644 apps/examples/src/8-error-boundary/ErrorBoundaryExample.tsx create mode 100644 apps/examples/src/8-error-boundary/ErrorShape.ts create mode 100644 apps/examples/src/8-error-boundary/ErrorUtil.ts rename apps/examples/src/end-to-end/{ForEndToEndTests.tsx => end-to-end.tsx} (91%) delete mode 100644 packages/editor/src/lib/config/SyncedStore.tsx delete mode 100644 packages/editor/src/lib/config/TldrawEditorConfig.tsx create mode 100644 packages/editor/src/lib/config/createTLStore.ts create mode 100644 packages/editor/src/lib/config/createTLUser.ts create mode 100644 packages/editor/src/lib/config/defaultShapes.ts create mode 100644 packages/editor/src/lib/config/defaultTools.ts create mode 100644 packages/editor/src/lib/hooks/useLocalStore.ts create mode 100644 packages/editor/src/lib/hooks/usePrevious.ts create mode 100644 packages/editor/src/lib/hooks/useTLStore.ts create mode 100644 packages/editor/src/lib/utils/sync/StoreWithStatus.ts rename packages/{tlsync-client/src/lib => editor/src/lib/utils/sync}/TLLocalSyncClient.test.ts (96%) rename packages/{tlsync-client/src/lib => editor/src/lib/utils/sync}/TLLocalSyncClient.ts (99%) rename packages/{tlsync-client/src/lib => editor/src/lib/utils/sync}/alerts.ts (100%) rename packages/{tlsync-client/src/lib => editor/src/lib/utils/sync}/hardReset.ts (100%) rename packages/{tlsync-client/src/lib => editor/src/lib/utils/sync}/indexedDb.ts (98%) rename packages/{tlsync-client/src/lib => editor/src/lib/utils/sync}/persistence-constants.ts (95%) delete mode 100644 packages/tlsync-client/CHANGELOG.md delete mode 100644 packages/tlsync-client/LICENSE delete mode 100644 packages/tlsync-client/README.md delete mode 100644 packages/tlsync-client/api-extractor.json delete mode 100644 packages/tlsync-client/api-report.md delete mode 100644 packages/tlsync-client/package.json delete mode 100644 packages/tlsync-client/setupJest.js delete mode 100644 packages/tlsync-client/src/index.ts delete mode 100644 packages/tlsync-client/src/lib/hooks/useLocalSyncClient.ts delete mode 100644 packages/tlsync-client/tsconfig.json 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: