From 1927f8804158ed4bc1df42eb8a08bdc6b305c379 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 12 Jun 2023 15:04:14 +0100 Subject: [PATCH] mini `defineShape` API (#1563) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on #1549, but with a lot of code-structure related changes backed out. Shape schemas are still defined in tlschemas with this diff. Couple differences between this and #1549: - This tightens up the relationship between store schemas and editor schemas a bit - Reduces the number of places we need to remember to include core shapes - Only `` sets default shapes by default. If you're doing something funky with lower-level APIs, you need to specify `defaultShapes` manually - Replaces `validator` with `props` for shapes ### Change Type - [x] `major` — Breaking Change ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [x] Unit Tests - [ ] Webdriver tests ### Release Notes [dev-facing, notes to come] --- .../src/14-persistence/PersistenceExample.tsx | 12 +- .../examples/src/3-custom-config/CardShape.ts | 9 - .../{CardShapeUtil.tsx => CardShape.tsx} | 15 +- .../3-custom-config/CustomConfigExample.tsx | 4 +- .../src/4-custom-ui/CustomUiExample.tsx | 4 +- .../src/5-exploded/ExplodedExample.tsx | 16 +- .../8-error-boundary/ErrorBoundaryExample.tsx | 9 +- .../src/8-error-boundary/ErrorShape.ts | 19 +- .../src/8-error-boundary/ErrorShapeUtil.ts | 17 -- apps/examples/src/yjs/useYjsStore.ts | 4 +- apps/vscode/editor/src/app.tsx | 19 +- apps/vscode/extension/src/file.ts | 6 +- packages/editor/api-report.md | 119 +++++----- packages/editor/src/index.ts | 4 +- packages/editor/src/lib/TldrawEditor.tsx | 55 +++-- .../editor/src/lib/config/createTLStore.ts | 31 ++- .../editor/src/lib/config/defaultShapes.ts | 138 ++++++++--- packages/editor/src/lib/config/defineShape.ts | 26 +++ packages/editor/src/lib/editor/Editor.ts | 72 +++--- .../ArrowShapeUtil/ArrowShapeUtil.tsx | 2 +- .../BookmarkShapeUtil/BookmarkShapeUtil.tsx | 2 +- .../DrawShapeUtil/DrawShapeUtil.tsx | 2 +- .../EmbedShapeUtil/EmbedShapeUtil.tsx | 2 +- .../FrameShapeUtil/FrameShapeUtil.tsx | 2 +- .../shapeutils/GeoShapeUtil/GeoShapeUtil.tsx | 2 +- .../GroupShapeUtil/GroupShapeUtil.tsx | 2 +- .../HighlightShapeUtil/HighlightShapeUtil.tsx | 2 +- .../ImageShapeUtil/ImageShapeUtil.tsx | 2 +- .../LineShapeUtil/LineShapeUtil.tsx | 2 +- .../NoteShapeUtil/NoteShapeUtil.tsx | 2 +- .../TextShapeUtil/TextShapeUtil.tsx | 2 +- .../VideoShapeUtil/VideoShapeUtil.tsx | 2 +- .../editor/src/lib/hooks/useLocalStore.ts | 10 +- packages/editor/src/lib/test/Editor.test.tsx | 61 ++++- packages/editor/src/lib/test/TestEditor.ts | 10 +- .../editor/src/lib/test/TldrawEditor.test.tsx | 216 +++++++++++++----- .../src/lib/test/tools/translating.test.ts | 12 +- .../lib/utils/sync/TLLocalSyncClient.test.ts | 3 +- .../src/lib/utils/sync/indexedDb.test.ts | 2 +- packages/file-format/api-report.md | 5 +- packages/file-format/src/lib/file.ts | 10 +- packages/file-format/src/test/file.test.ts | 26 +-- packages/tldraw/src/lib/Tldraw.test.tsx | 6 +- packages/tldraw/src/lib/Tldraw.tsx | 19 +- packages/tlschema/api-report.md | 131 +++++++++-- packages/tlschema/src/createTLSchema.ts | 109 +-------- packages/tlschema/src/index.ts | 68 ++++-- packages/tlschema/src/misc/TLColor.ts | 6 +- packages/tlschema/src/misc/TLCursor.ts | 6 +- packages/tlschema/src/misc/TLScribble.ts | 6 +- packages/tlschema/src/shapes/TLArrowShape.ts | 33 ++- packages/tlschema/src/shapes/TLBaseShape.ts | 9 +- .../tlschema/src/shapes/TLBookmarkShape.ts | 17 +- packages/tlschema/src/shapes/TLDrawShape.ts | 25 +- packages/tlschema/src/shapes/TLEmbedShape.ts | 31 ++- packages/tlschema/src/shapes/TLFrameShape.ts | 15 +- packages/tlschema/src/shapes/TLGeoShape.ts | 37 ++- packages/tlschema/src/shapes/TLGroupShape.ts | 8 +- .../tlschema/src/shapes/TLHighlightShape.ts | 19 +- packages/tlschema/src/shapes/TLIconShape.ts | 19 +- packages/tlschema/src/shapes/TLImageShape.ts | 21 +- packages/tlschema/src/shapes/TLLineShape.ts | 19 +- packages/tlschema/src/shapes/TLNoteShape.ts | 25 +- packages/tlschema/src/shapes/TLTextShape.ts | 25 +- packages/tlschema/src/shapes/TLVideoShape.ts | 21 +- packages/utils/api-report.md | 19 ++ packages/utils/src/index.ts | 5 +- packages/utils/src/lib/function.ts | 7 + packages/utils/src/lib/object.ts | 17 ++ packages/utils/src/lib/types.ts | 8 + packages/validate/api-report.md | 34 +-- packages/validate/src/lib/validation.ts | 29 +-- 72 files changed, 1081 insertions(+), 673 deletions(-) delete mode 100644 apps/examples/src/3-custom-config/CardShape.ts rename apps/examples/src/3-custom-config/{CardShapeUtil.tsx => CardShape.tsx} (82%) delete mode 100644 apps/examples/src/8-error-boundary/ErrorShapeUtil.ts create mode 100644 packages/editor/src/lib/config/defineShape.ts diff --git a/apps/examples/src/14-persistence/PersistenceExample.tsx b/apps/examples/src/14-persistence/PersistenceExample.tsx index 8f0e2daff..8316d6a16 100644 --- a/apps/examples/src/14-persistence/PersistenceExample.tsx +++ b/apps/examples/src/14-persistence/PersistenceExample.tsx @@ -1,4 +1,4 @@ -import { Canvas, ContextMenu, TldrawEditor, TldrawUi, createTLStore } from '@tldraw/tldraw' +import { Tldraw, createTLStore, defaultShapes } from '@tldraw/tldraw' import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/ui.css' import { throttle } from '@tldraw/utils' @@ -7,7 +7,7 @@ import { useLayoutEffect, useState } from 'react' const PERSISTENCE_KEY = 'example-3' export default function PersistenceExample() { - const [store] = useState(() => createTLStore()) + const [store] = useState(() => createTLStore({ shapes: defaultShapes })) const [loadingState, setLoadingState] = useState< { status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string } >({ @@ -64,13 +64,7 @@ export default function PersistenceExample() { return (
- - - - - - - +
) } diff --git a/apps/examples/src/3-custom-config/CardShape.ts b/apps/examples/src/3-custom-config/CardShape.ts deleted file mode 100644 index 356d586ff..000000000 --- a/apps/examples/src/3-custom-config/CardShape.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { TLBaseShape } from '@tldraw/tldraw' - -export type CardShape = TLBaseShape< - 'card', - { - w: number - h: number - } -> diff --git a/apps/examples/src/3-custom-config/CardShapeUtil.tsx b/apps/examples/src/3-custom-config/CardShape.tsx similarity index 82% rename from apps/examples/src/3-custom-config/CardShapeUtil.tsx rename to apps/examples/src/3-custom-config/CardShape.tsx index 469ec28ba..22059eb4d 100644 --- a/apps/examples/src/3-custom-config/CardShapeUtil.tsx +++ b/apps/examples/src/3-custom-config/CardShape.tsx @@ -1,5 +1,12 @@ -import { BaseBoxShapeUtil, HTMLContainer } from '@tldraw/tldraw' -import { CardShape } from './CardShape' +import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape, defineShape } from '@tldraw/tldraw' + +export type CardShape = TLBaseShape< + 'card', + { + w: number + h: number + } +> export class CardShapeUtil extends BaseBoxShapeUtil { // Id — the shape util's id @@ -43,3 +50,7 @@ export class CardShapeUtil extends BaseBoxShapeUtil { return } } + +export const CardShape = defineShape('card', { + util: CardShapeUtil, +}) diff --git a/apps/examples/src/3-custom-config/CustomConfigExample.tsx b/apps/examples/src/3-custom-config/CustomConfigExample.tsx index 9d9140e0a..0a090406e 100644 --- a/apps/examples/src/3-custom-config/CustomConfigExample.tsx +++ b/apps/examples/src/3-custom-config/CustomConfigExample.tsx @@ -1,10 +1,10 @@ import { TLUiMenuGroup, Tldraw, menuItem, toolbarItem } from '@tldraw/tldraw' import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/ui.css' +import { CardShape } from './CardShape' import { CardShapeTool } from './CardShapeTool' -import { CardShapeUtil } from './CardShapeUtil' -const shapes = { card: { util: CardShapeUtil } } +const shapes = [CardShape] const tools = [CardShapeTool] export default function CustomConfigExample() { diff --git a/apps/examples/src/4-custom-ui/CustomUiExample.tsx b/apps/examples/src/4-custom-ui/CustomUiExample.tsx index e8c3b036f..470b42359 100644 --- a/apps/examples/src/4-custom-ui/CustomUiExample.tsx +++ b/apps/examples/src/4-custom-ui/CustomUiExample.tsx @@ -1,4 +1,4 @@ -import { Canvas, TldrawEditor, useEditor } from '@tldraw/tldraw' +import { Canvas, TldrawEditor, defaultShapes, defaultTools, useEditor } from '@tldraw/tldraw' import '@tldraw/tldraw/editor.css' import { useEffect } from 'react' import { track } from 'signia-react' @@ -7,7 +7,7 @@ import './custom-ui.css' export default function CustomUiExample() { return (
- + diff --git a/apps/examples/src/5-exploded/ExplodedExample.tsx b/apps/examples/src/5-exploded/ExplodedExample.tsx index ad85d9400..147336172 100644 --- a/apps/examples/src/5-exploded/ExplodedExample.tsx +++ b/apps/examples/src/5-exploded/ExplodedExample.tsx @@ -1,11 +1,23 @@ -import { Canvas, ContextMenu, TldrawEditor, TldrawUi } from '@tldraw/tldraw' +import { + Canvas, + ContextMenu, + TldrawEditor, + TldrawUi, + defaultShapes, + defaultTools, +} from '@tldraw/tldraw' import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/ui.css' export default function ExplodedExample() { return (
- + diff --git a/apps/examples/src/8-error-boundary/ErrorBoundaryExample.tsx b/apps/examples/src/8-error-boundary/ErrorBoundaryExample.tsx index b483ca89d..96db9c2af 100644 --- a/apps/examples/src/8-error-boundary/ErrorBoundaryExample.tsx +++ b/apps/examples/src/8-error-boundary/ErrorBoundaryExample.tsx @@ -1,19 +1,16 @@ import { createShapeId, Tldraw } from '@tldraw/tldraw' import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/ui.css' -import { ErrorShapeUtil } from './ErrorShapeUtil' +import { ErrorShape } from './ErrorShape' -const shapes = { - error: { - util: ErrorShapeUtil, // a custom shape that will always error - }, -} +const shapes = [ErrorShape] export default function ErrorBoundaryExample() { return (
Shape error! {String(error)}
, // use a custom error fallback for shapes diff --git a/apps/examples/src/8-error-boundary/ErrorShape.ts b/apps/examples/src/8-error-boundary/ErrorShape.ts index 37e90a5d1..29700f37d 100644 --- a/apps/examples/src/8-error-boundary/ErrorShape.ts +++ b/apps/examples/src/8-error-boundary/ErrorShape.ts @@ -1,3 +1,20 @@ -import { TLBaseShape } from '@tldraw/tldraw' +import { BaseBoxShapeUtil, TLBaseShape, defineShape } from '@tldraw/tldraw' export type ErrorShape = TLBaseShape<'error', { w: number; h: number; message: string }> + +export class ErrorShapeUtil extends BaseBoxShapeUtil { + static override type = 'error' as const + 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!`) + } +} + +export const ErrorShape = defineShape('error', { util: ErrorShapeUtil }) diff --git a/apps/examples/src/8-error-boundary/ErrorShapeUtil.ts b/apps/examples/src/8-error-boundary/ErrorShapeUtil.ts deleted file mode 100644 index 10f5e8429..000000000 --- a/apps/examples/src/8-error-boundary/ErrorShapeUtil.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BaseBoxShapeUtil } from '@tldraw/tldraw' -import { ErrorShape } from './ErrorShape' - -export class ErrorShapeUtil extends BaseBoxShapeUtil { - 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/yjs/useYjsStore.ts b/apps/examples/src/yjs/useYjsStore.ts index e13e37d9d..b7227ffef 100644 --- a/apps/examples/src/yjs/useYjsStore.ts +++ b/apps/examples/src/yjs/useYjsStore.ts @@ -1,4 +1,4 @@ -import { TLStoreWithStatus, createTLStore } from '@tldraw/tldraw' +import { TLStoreWithStatus, createTLStore, defaultShapes } from '@tldraw/tldraw' import { useEffect, useState } from 'react' import { initializeStoreFromYjsDoc, @@ -14,7 +14,7 @@ export function useYjsStore() { const [storeWithStatus, setStoreWithStatus] = useState({ status: 'loading' }) useEffect(() => { - const store = createTLStore() + const store = createTLStore({ shapes: defaultShapes }) initializeStoreFromYjsDoc(store) syncYjsDocChangesToStore(store) syncStoreChangesToYjsDoc(store) diff --git a/apps/vscode/editor/src/app.tsx b/apps/vscode/editor/src/app.tsx index 3a908f0ea..1fd2e95b2 100644 --- a/apps/vscode/editor/src/app.tsx +++ b/apps/vscode/editor/src/app.tsx @@ -1,4 +1,12 @@ -import { Canvas, Editor, ErrorBoundary, TldrawEditor, setRuntimeOverrides } from '@tldraw/editor' +import { + Canvas, + Editor, + ErrorBoundary, + TldrawEditor, + defaultShapes, + defaultTools, + setRuntimeOverrides, +} from '@tldraw/editor' import { linksUiOverrides } from './utils/links' // eslint-disable-next-line import/no-internal-modules import '@tldraw/editor/editor.css' @@ -124,7 +132,14 @@ function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerPro }, []) return ( - + {/* */} diff --git a/apps/vscode/extension/src/file.ts b/apps/vscode/extension/src/file.ts index 505b8fc03..03fb69473 100644 --- a/apps/vscode/extension/src/file.ts +++ b/apps/vscode/extension/src/file.ts @@ -1,17 +1,17 @@ -import { createTLSchema } from '@tldraw/editor' +import { createTLStore, defaultShapes } from '@tldraw/editor' import { TldrawFile } from '@tldraw/file-format' import * as vscode from 'vscode' import { nicelog } from './utils' export const defaultFileContents: TldrawFile = { tldrawFileFormatVersion: 1, - schema: createTLSchema().serialize(), + schema: createTLStore({ shapes: defaultShapes }).schema.serialize(), records: [], } export const fileContentWithErrors: TldrawFile = { tldrawFileFormatVersion: 1, - schema: createTLSchema().serialize(), + schema: createTLStore({ shapes: defaultShapes }).schema.serialize(), records: [{ typeName: 'shape', id: null } as any], } diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 3372c3d12..b6538fadd 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -30,7 +30,9 @@ import { SelectionCorner } from '@tldraw/primitives'; import { SelectionEdge } from '@tldraw/primitives'; import { SelectionHandle } from '@tldraw/primitives'; import { SerializedSchema } from '@tldraw/store'; +import { ShapeProps } from '@tldraw/tlschema'; import { Signal } from 'signia'; +import { StoreSchema } from '@tldraw/store'; import { StoreSnapshot } from '@tldraw/store'; import { StrokePoint } from '@tldraw/primitives'; import { TLAlignType } from '@tldraw/tlschema'; @@ -77,6 +79,7 @@ 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'; @@ -166,7 +169,7 @@ export class ArrowShapeUtil extends ShapeUtil { // (undocumented) toSvg(shape: TLArrowShape, font: string, colors: TLExportColors): SVGGElement; // (undocumented) - static type: string; + static type: "arrow"; } // @public (undocumented) @@ -221,7 +224,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil { // (undocumented) render(shape: TLBookmarkShape): JSX.Element; // (undocumented) - static type: string; + static type: "bookmark"; // (undocumented) protected updateBookmarkAsset: { (shape: TLBookmarkShape): Promise; @@ -243,6 +246,9 @@ export const checkFlag: (flag: (() => boolean) | boolean | undefined) => boolean // @public export function containBoxSize(originalSize: BoxWidthHeight, containBoxSize: BoxWidthHeight): BoxWidthHeight; +// @public (undocumented) +export const coreShapes: readonly [TLShapeInfo, TLShapeInfo, TLShapeInfo, TLShapeInfo, TLShapeInfo]; + // @public (undocumented) export function correctSpacesToNbsp(input: string): string; @@ -250,7 +256,7 @@ export function correctSpacesToNbsp(input: string): string; export function createSessionStateSnapshotSignal(store: TLStore): Signal; // @public -export function createTLStore(opts?: TLStoreOptions): TLStore; +export function createTLStore({ initialData, defaultName, ...rest }: TLStoreOptions): TLStore; // @public (undocumented) export function dataTransferItemAsString(item: DataTransferItem): Promise; @@ -298,11 +304,14 @@ export function defaultEmptyAs(str: string, dflt: string): string; export const DefaultErrorFallback: TLErrorFallbackComponent; // @public (undocumented) -export const defaultShapes: Record; +export const defaultShapes: readonly [TLShapeInfo, TLShapeInfo, TLShapeInfo, TLShapeInfo, TLShapeInfo, TLShapeInfo, TLShapeInfo, TLShapeInfo]; // @public (undocumented) export const defaultTools: TLStateNodeConstructor[]; +// @public (undocumented) +export function defineShape(type: T['type'], opts: Omit, 'type'>): TLShapeInfo; + // @internal (undocumented) export const DOUBLE_CLICK_DURATION = 450; @@ -347,12 +356,12 @@ export class DrawShapeUtil extends ShapeUtil { // (undocumented) toSvg(shape: TLDrawShape, _font: string | undefined, colors: TLExportColors): SVGGElement; // (undocumented) - static type: string; + static type: "draw"; } // @public (undocumented) export class Editor extends EventEmitter { - constructor({ store, user, tools, shapes, getContainer, }: TLEditorOptions); + constructor({ store, user, shapes, tools, getContainer }: TLEditorOptions); addOpenMenu(id: string): this; alignShapes(operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top', ids?: TLShapeId[]): this; get allShapesCommonBounds(): Box2d | null; @@ -792,7 +801,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil { // (undocumented) render(shape: TLEmbedShape): JSX.Element; // (undocumented) - static type: string; + static type: "embed"; } // @public (undocumented) @@ -867,7 +876,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil { // (undocumented) toSvg(shape: TLFrameShape, font: string, colors: TLExportColors): Promise | SVGElement; // (undocumented) - static type: string; + static type: "frame"; } // @public (undocumented) @@ -985,7 +994,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { // (undocumented) toSvg(shape: TLGeoShape, font: string, colors: TLExportColors): SVGElement; // (undocumented) - static type: string; + static type: "geo"; } // @public @@ -1104,7 +1113,7 @@ export class GroupShapeUtil extends ShapeUtil { // (undocumented) render(shape: TLGroupShape): JSX.Element | null; // (undocumented) - static type: string; + static type: "group"; // (undocumented) type: "group"; } @@ -1160,7 +1169,7 @@ export class HighlightShapeUtil extends ShapeUtil { // (undocumented) toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors): SVGPathElement; // (undocumented) - static type: string; + static type: "highlight"; } // @public (undocumented) @@ -1191,7 +1200,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil { // (undocumented) toSvg(shape: TLImageShape): Promise; // (undocumented) - static type: string; + static type: "image"; } // @public (undocumented) @@ -1261,7 +1270,7 @@ export class LineShapeUtil extends ShapeUtil { // (undocumented) toSvg(shape: TLLineShape, _font: string, colors: TLExportColors): SVGGElement; // (undocumented) - static type: string; + static type: "line"; } // @public (undocumented) @@ -1281,6 +1290,17 @@ export const MAJOR_NUDGE_FACTOR = 10; // @public (undocumented) export function matchEmbedUrl(url: string): { definition: { + readonly type: "tldraw"; + readonly title: "tldraw"; + readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"]; + readonly minWidth: 300; + readonly minHeight: 300; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { readonly type: "codepen"; readonly title: "Codepen"; readonly hostnames: readonly ["codepen.io"]; @@ -1289,7 +1309,7 @@ export function matchEmbedUrl(url: string): { readonly width: 520; readonly height: 400; readonly doesResize: true; - readonly toEmbedUrl: (url: string) => string | undefined; + readonly toEmbedUrl: (url: string) => string | undefined; /** @public */ readonly fromEmbedUrl: (url: string) => string | undefined; } | { readonly type: "codesandbox"; @@ -1411,17 +1431,6 @@ export function matchEmbedUrl(url: string): { readonly doesResize: true; readonly toEmbedUrl: (url: string) => string | undefined; readonly fromEmbedUrl: (url: string) => string | undefined; - } | { - readonly type: "tldraw"; - readonly title: "tldraw"; - readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"]; - readonly minWidth: 300; - readonly minHeight: 300; - readonly width: 720; - readonly height: 500; - readonly doesResize: true; - readonly toEmbedUrl: (url: string) => string | undefined; - readonly fromEmbedUrl: (url: string) => string | undefined; } | { readonly type: "vimeo"; readonly title: "Vimeo"; @@ -1453,6 +1462,17 @@ export function matchEmbedUrl(url: string): { // @public (undocumented) export function matchUrl(url: string): { definition: { + readonly type: "tldraw"; + readonly title: "tldraw"; + readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"]; + readonly minWidth: 300; + readonly minHeight: 300; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { readonly type: "codepen"; readonly title: "Codepen"; readonly hostnames: readonly ["codepen.io"]; @@ -1461,7 +1481,7 @@ export function matchUrl(url: string): { readonly width: 520; readonly height: 400; readonly doesResize: true; - readonly toEmbedUrl: (url: string) => string | undefined; + readonly toEmbedUrl: (url: string) => string | undefined; /** @public */ readonly fromEmbedUrl: (url: string) => string | undefined; } | { readonly type: "codesandbox"; @@ -1583,17 +1603,6 @@ export function matchUrl(url: string): { readonly doesResize: true; readonly toEmbedUrl: (url: string) => string | undefined; readonly fromEmbedUrl: (url: string) => string | undefined; - } | { - readonly type: "tldraw"; - readonly title: "tldraw"; - readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"]; - readonly minWidth: 300; - readonly minHeight: 300; - readonly width: 720; - readonly height: 500; - readonly doesResize: true; - readonly toEmbedUrl: (url: string) => string | undefined; - readonly fromEmbedUrl: (url: string) => string | undefined; } | { readonly type: "vimeo"; readonly title: "Vimeo"; @@ -1731,7 +1740,7 @@ export class NoteShapeUtil extends ShapeUtil { // (undocumented) toSvg(shape: TLNoteShape, font: string, colors: TLExportColors): SVGGElement; // (undocumented) - static type: string; + static type: "note"; } // @public (undocumented) @@ -2092,7 +2101,7 @@ export class TextShapeUtil extends ShapeUtil { // (undocumented) toSvg(shape: TLTextShape, font: string | undefined, colors: TLExportColors): SVGGElement; // (undocumented) - static type: string; + static type: "text"; } // @public (undocumented) @@ -2191,8 +2200,8 @@ export const TldrawEditor: React_2.NamedExoticComponent; // @public (undocumented) export type TldrawEditorProps = { children?: any; - shapes?: Record; - tools?: TLStateNodeConstructor[]; + shapes?: readonly AnyTLShapeInfo[]; + tools?: readonly TLStateNodeConstructor[]; assetUrls?: RecursivePartial; autoFocus?: boolean; components?: Partial; @@ -2260,9 +2269,9 @@ export interface TLEditorComponents { // @public (undocumented) export interface TLEditorOptions { getContainer: () => HTMLElement; - shapes?: Record; + shapes: readonly AnyTLShapeInfo[]; store: TLStore; - tools?: TLStateNodeConstructor[]; + tools: readonly TLStateNodeConstructor[]; user?: TLUser; } @@ -2596,12 +2605,11 @@ export interface TLSessionStateSnapshot { } // @public (undocumented) -export type TLShapeInfo = { - util: TLShapeUtilConstructor; +export type TLShapeInfo = { + type: T['type']; + util: TLShapeUtilConstructor; + props?: ShapeProps; migrations?: Migrations; - validator?: { - validate: (record: any) => any; - }; }; // @public (undocumented) @@ -2634,10 +2642,13 @@ export type TLStoreEventInfo = HistoryEntry; // @public (undocumented) export type TLStoreOptions = { - customShapes?: Record; initialData?: StoreSnapshot; defaultName?: string; -}; +} & ({ + schema: StoreSchema; +} | { + shapes: readonly AnyTLShapeInfo[]; +}); // @public (undocumented) export type TLStoreWithStatus = { @@ -2713,9 +2724,9 @@ export function useContainer(): HTMLDivElement; export const useEditor: () => Editor; // @internal (undocumented) -export function useLocalStore(opts?: { - persistenceKey?: string | undefined; - sessionId?: string | undefined; +export function useLocalStore({ persistenceKey, sessionId, ...rest }: { + persistenceKey?: string; + sessionId?: string; } & TLStoreOptions): TLStoreWithStatus; // @internal (undocumented) @@ -2754,7 +2765,7 @@ export class VideoShapeUtil extends BaseBoxShapeUtil { // (undocumented) toSvg(shape: TLVideoShape): SVGGElement; // (undocumented) - static type: string; + static type: "video"; } // @internal (undocumented) diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index b4d83c797..94d03c5e2 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -41,12 +41,12 @@ export { } from './lib/config/TLUserPreferences' export { createTLStore, - type TLShapeInfo, type TLStoreEventInfo, type TLStoreOptions, } from './lib/config/createTLStore' -export { defaultShapes } from './lib/config/defaultShapes' +export { coreShapes, defaultShapes } from './lib/config/defaultShapes' export { defaultTools } from './lib/config/defaultTools' +export { defineShape, type TLShapeInfo } from './lib/config/defineShape' export { ANIMATION_MEDIUM_MS, ANIMATION_SHORT_MS, diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index 3c2e0e345..688261232 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -1,11 +1,11 @@ import { Store, StoreSnapshot } from '@tldraw/store' import { TLRecord, TLStore } from '@tldraw/tlschema' -import { RecursivePartial, annotateError } from '@tldraw/utils' +import { RecursivePartial, Required, annotateError } from '@tldraw/utils' import React, { memo, useCallback, useLayoutEffect, useState, useSyncExternalStore } from 'react' import { TLEditorAssetUrls, useDefaultEditorAssetsWithOverrides } from './assetUrls' import { DefaultErrorFallback } from './components/DefaultErrorFallback' import { OptionalErrorBoundary } from './components/ErrorBoundary' -import { TLShapeInfo } from './config/createTLStore' +import { AnyTLShapeInfo } from './config/defineShape' import { Editor } from './editor/Editor' import { TLStateNodeConstructor } from './editor/tools/StateNode' import { ContainerProvider, useContainer } from './hooks/useContainer' @@ -31,11 +31,11 @@ export type TldrawEditorProps = { /** * An array of shape utils to use in the editor. */ - shapes?: Record + shapes?: readonly AnyTLShapeInfo[] /** * An array of tools to use in the editor. */ - tools?: TLStateNodeConstructor[] + tools?: readonly TLStateNodeConstructor[] /** * Urls for where to find fonts and other assets. */ @@ -105,16 +105,28 @@ declare global { } } +const EMPTY_SHAPES_ARRAY = [] as const +const EMPTY_TOOLS_ARRAY = [] as const + /** @public */ -export const TldrawEditor = memo(function TldrawEditor(props: TldrawEditorProps) { +export const TldrawEditor = memo(function TldrawEditor({ + store, + components, + ...rest +}: TldrawEditorProps) { const [container, setContainer] = React.useState(null) const ErrorFallback = - props.components?.ErrorFallback === undefined - ? DefaultErrorFallback - : props.components?.ErrorFallback + components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback - const { store, ...rest } = props + // apply defaults. if you're using the bare @tldraw/editor package, we + // default these to the "tldraw zero" configuration. We have different + // defaults applied in @tldraw/tldraw. + const withDefaults = { + ...rest, + shapes: rest.shapes ?? EMPTY_SHAPES_ARRAY, + tools: rest.tools ?? EMPTY_TOOLS_ARRAY, + } return (
@@ -124,18 +136,18 @@ export const TldrawEditor = memo(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 - + )} @@ -145,11 +157,13 @@ export const TldrawEditor = memo(function TldrawEditor(props: TldrawEditorProps) ) }) -function TldrawEditorWithOwnStore(props: TldrawEditorProps & { store: undefined }) { +function TldrawEditorWithOwnStore( + props: Required +) { const { defaultName, initialData, shapes, persistenceKey, sessionId } = props const syncedStore = useLocalStore({ - customShapes: shapes, + shapes, initialData, persistenceKey, sessionId, @@ -163,7 +177,7 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({ store, assetUrls, ...rest -}: TldrawEditorProps & { store: TLStoreWithStatus }) { +}: Required) { const assets = useDefaultEditorAssetsWithOverrides(assetUrls) const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(assets) @@ -206,9 +220,12 @@ function TldrawEditorWithReadyStore({ tools, shapes, autoFocus, -}: TldrawEditorProps & { - store: TLStore -}) { +}: Required< + TldrawEditorProps & { + store: TLStore + }, + 'shapes' | 'tools' +>) { const { ErrorFallback } = useEditorComponents() const container = useContainer() const [editor, setEditor] = useState(null) diff --git a/packages/editor/src/lib/config/createTLStore.ts b/packages/editor/src/lib/config/createTLStore.ts index eb426ff92..3fba6ed7a 100644 --- a/packages/editor/src/lib/config/createTLStore.ts +++ b/packages/editor/src/lib/config/createTLStore.ts @@ -1,20 +1,13 @@ -import { HistoryEntry, Migrations, Store, StoreSnapshot } from '@tldraw/store' -import { TLRecord, TLStore, createTLSchema } from '@tldraw/tlschema' -import { TLShapeUtilConstructor } from '../editor/shapeutils/ShapeUtil' - -/** @public */ -export type TLShapeInfo = { - util: TLShapeUtilConstructor - migrations?: Migrations - validator?: { validate: (record: any) => any } -} +import { HistoryEntry, Store, StoreSchema, StoreSnapshot } from '@tldraw/store' +import { TLRecord, TLStore, TLStoreProps, createTLSchema } from '@tldraw/tlschema' +import { checkShapesAndAddCore } from './defaultShapes' +import { AnyTLShapeInfo, TLShapeInfo } from './defineShape' /** @public */ export type TLStoreOptions = { - customShapes?: Record initialData?: StoreSnapshot defaultName?: string -} +} & ({ shapes: readonly AnyTLShapeInfo[] } | { schema: StoreSchema }) /** @public */ export type TLStoreEventInfo = HistoryEntry @@ -25,14 +18,20 @@ export type TLStoreEventInfo = HistoryEntry * @param opts - Options for creating the store. * * @public */ -export function createTLStore(opts = {} as TLStoreOptions): TLStore { - const { customShapes = {}, initialData, defaultName = '' } = opts - +export function createTLStore({ initialData, defaultName = '', ...rest }: TLStoreOptions): TLStore { + const schema = + 'schema' in rest + ? rest.schema + : createTLSchema({ shapes: shapesArrayToShapeMap(checkShapesAndAddCore(rest.shapes)) }) return new Store({ - schema: createTLSchema({ customShapes }), + schema, initialData, props: { defaultName, }, }) } + +function shapesArrayToShapeMap(shapes: TLShapeInfo[]) { + return Object.fromEntries(shapes.map((s) => [s.type, s])) +} diff --git a/packages/editor/src/lib/config/defaultShapes.ts b/packages/editor/src/lib/config/defaultShapes.ts index 780500193..c56007cdf 100644 --- a/packages/editor/src/lib/config/defaultShapes.ts +++ b/packages/editor/src/lib/config/defaultShapes.ts @@ -1,3 +1,31 @@ +import { + arrowShapeMigrations, + arrowShapeProps, + bookmarkShapeMigrations, + bookmarkShapeProps, + drawShapeMigrations, + drawShapeProps, + embedShapeMigrations, + embedShapeProps, + frameShapeMigrations, + frameShapeProps, + geoShapeMigrations, + geoShapeProps, + groupShapeMigrations, + groupShapeProps, + highlightShapeMigrations, + highlightShapeProps, + imageShapeMigrations, + imageShapeProps, + lineShapeMigrations, + lineShapeProps, + noteShapeMigrations, + noteShapeProps, + textShapeMigrations, + textShapeProps, + videoShapeMigrations, + videoShapeProps, +} from '@tldraw/tlschema' import { ArrowShapeUtil } from '../editor/shapeutils/ArrowShapeUtil/ArrowShapeUtil' import { BookmarkShapeUtil } from '../editor/shapeutils/BookmarkShapeUtil/BookmarkShapeUtil' import { DrawShapeUtil } from '../editor/shapeutils/DrawShapeUtil/DrawShapeUtil' @@ -11,57 +39,103 @@ import { LineShapeUtil } from '../editor/shapeutils/LineShapeUtil/LineShapeUtil' import { NoteShapeUtil } from '../editor/shapeutils/NoteShapeUtil/NoteShapeUtil' import { TextShapeUtil } from '../editor/shapeutils/TextShapeUtil/TextShapeUtil' import { VideoShapeUtil } from '../editor/shapeutils/VideoShapeUtil/VideoShapeUtil' -import { TLShapeInfo } from './createTLStore' +import { AnyTLShapeInfo, TLShapeInfo, defineShape } from './defineShape' /** @public */ -export const coreShapes: Record = { +export const coreShapes = [ // created by grouping interactions, probably the corest core shape that we have - group: { + defineShape('group', { util: GroupShapeUtil, - }, + props: groupShapeProps, + migrations: groupShapeMigrations, + }), // created by embed menu / url drop - embed: { + defineShape('embed', { util: EmbedShapeUtil, - }, + props: embedShapeProps, + migrations: embedShapeMigrations, + }), // created by copy and paste / url drop - bookmark: { + defineShape('bookmark', { util: BookmarkShapeUtil, - }, + props: bookmarkShapeProps, + migrations: bookmarkShapeMigrations, + }), // created by copy and paste / file drop - image: { + defineShape('image', { util: ImageShapeUtil, - }, - // created by copy and paste / file drop - video: { - util: VideoShapeUtil, - }, + props: imageShapeProps, + migrations: imageShapeMigrations, + }), // created by copy and paste - text: { + defineShape('text', { util: TextShapeUtil, - }, -} + props: textShapeProps, + migrations: textShapeMigrations, + }), +] as const /** @public */ -export const defaultShapes: Record = { - draw: { +export const defaultShapes = [ + defineShape('draw', { util: DrawShapeUtil, - }, - geo: { + props: drawShapeProps, + migrations: drawShapeMigrations, + }), + defineShape('geo', { util: GeoShapeUtil, - }, - line: { + props: geoShapeProps, + migrations: geoShapeMigrations, + }), + defineShape('line', { util: LineShapeUtil, - }, - note: { + props: lineShapeProps, + migrations: lineShapeMigrations, + }), + defineShape('note', { util: NoteShapeUtil, - }, - frame: { + props: noteShapeProps, + migrations: noteShapeMigrations, + }), + defineShape('frame', { util: FrameShapeUtil, - }, - arrow: { + props: frameShapeProps, + migrations: frameShapeMigrations, + }), + defineShape('arrow', { util: ArrowShapeUtil, - }, - highlight: { + props: arrowShapeProps, + migrations: arrowShapeMigrations, + }), + defineShape('highlight', { util: HighlightShapeUtil, - }, + props: highlightShapeProps, + migrations: highlightShapeMigrations, + }), + defineShape('video', { + util: VideoShapeUtil, + props: videoShapeProps, + migrations: videoShapeMigrations, + }), +] as const + +const coreShapeTypes = new Set(coreShapes.map((s) => s.type)) +export function checkShapesAndAddCore(customShapes: readonly TLShapeInfo[]) { + const shapes: AnyTLShapeInfo[] = [...coreShapes] + + const addedCustomShapeTypes = new Set() + for (const customShape of customShapes) { + if (coreShapeTypes.has(customShape.type)) { + throw new Error( + `Shape type "${customShape.type}" is a core shapes type and cannot be overridden` + ) + } + if (addedCustomShapeTypes.has(customShape.type)) { + throw new Error(`Shape type "${customShape.type}" is defined more than once`) + } + shapes.push(customShape) + addedCustomShapeTypes.add(customShape.type) + } + + return shapes } diff --git a/packages/editor/src/lib/config/defineShape.ts b/packages/editor/src/lib/config/defineShape.ts new file mode 100644 index 000000000..4717b4f98 --- /dev/null +++ b/packages/editor/src/lib/config/defineShape.ts @@ -0,0 +1,26 @@ +import { Migrations } from '@tldraw/store' +import { ShapeProps, TLBaseShape, TLUnknownShape } from '@tldraw/tlschema' +import { assert } from '@tldraw/utils' +import { TLShapeUtilConstructor } from '../editor/shapeutils/ShapeUtil' + +/** @public */ +export type TLShapeInfo = { + type: T['type'] + util: TLShapeUtilConstructor + props?: ShapeProps + migrations?: Migrations +} + +export type AnyTLShapeInfo = TLShapeInfo> + +/** @public */ +export function defineShape( + type: T['type'], + opts: Omit, 'type'> +): TLShapeInfo { + assert( + type === opts.util.type, + `Shape type "${type}" does not match util type "${opts.util.type}"` + ) + return { type, ...opts } +} diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index d6f87466d..d402ca56f 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -66,9 +66,11 @@ import { } from '@tldraw/tlschema' import { annotateError, + assert, compact, dedupe, deepCopy, + getOwnProperty, partition, sortById, structuredClone, @@ -76,10 +78,9 @@ import { import { EventEmitter } from 'eventemitter3' import { nanoid } from 'nanoid' import { EMPTY_ARRAY, atom, computed, transact } from 'signia' -import { TLShapeInfo } from '../config/createTLStore' import { TLUser, createTLUser } from '../config/createTLUser' -import { coreShapes, defaultShapes } from '../config/defaultShapes' -import { defaultTools } from '../config/defaultTools' +import { checkShapesAndAddCore } from '../config/defaultShapes' +import { AnyTLShapeInfo } from '../config/defineShape' import { ANIMATION_MEDIUM_MS, BLACKLISTED_PROPS, @@ -164,11 +165,11 @@ export interface TLEditorOptions { /** * An array of shapes to use in the editor. These will be used to create and manage shapes in the editor. */ - shapes?: Record + shapes: readonly AnyTLShapeInfo[] /** * An array of tools to use in the editor. These will be used to handle events and manage user interactions in the editor. */ - tools?: TLStateNodeConstructor[] + tools: readonly TLStateNodeConstructor[] /** * A user defined externally to replace the default user. */ @@ -182,13 +183,7 @@ export interface TLEditorOptions { /** @public */ export class Editor extends EventEmitter { - constructor({ - store, - user, - tools = defaultTools, - shapes = defaultShapes, - getContainer, - }: TLEditorOptions) { + constructor({ store, user, shapes, tools, getContainer }: TLEditorOptions) { super() this.store = store @@ -201,22 +196,29 @@ export class Editor extends EventEmitter { 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)]) - ) + const allShapes = checkShapesAndAddCore(shapes) - for (const [type, { util: Util }] of Object.entries(shapes)) { - if (shapeUtils[type]) { - throw Error(`May not overwrite core shape of type "${type}".`) + const shapeTypesInSchema = new Set( + Object.keys(store.schema.types.shape.migrations.subTypeMigrations!) + ) + for (const shape of allShapes) { + if (!shapeTypesInSchema.has(shape.type)) { + throw Error( + `Editor and store have different shapes: "${shape.type}" was passed into the editor but not the schema` + ) } - 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) + shapeTypesInSchema.delete(shape.type) } - this.shapeUtils = shapeUtils + if (shapeTypesInSchema.size > 0) { + throw Error( + `Editor and store have different shapes: "${ + [...shapeTypesInSchema][0] + }" is present in the store schema but not provided to the editor` + ) + } + this.shapeUtils = Object.fromEntries( + allShapes.map(({ util: Util }) => [Util.type, new Util(this, Util.type)]) + ) // Tools. // Accept tools from constructor parameters which may not conflict with the root note's default or @@ -976,12 +978,24 @@ export class Editor extends EventEmitter { * @public */ getShapeUtil(shape: S | TLShapePartial): ShapeUtil - getShapeUtil({ - type, - }: { + getShapeUtil(shapeUtilConstructor: { type: T extends ShapeUtil ? R['type'] : string }): T { - return this.shapeUtils[type] as T + const shapeUtil = getOwnProperty(this.shapeUtils, shapeUtilConstructor.type) as T | undefined + assert(shapeUtil, `No shape util found for type "${shapeUtilConstructor.type}"`) + + // does shapeUtilConstructor extends ShapeUtil? + if ( + 'prototype' in shapeUtilConstructor && + shapeUtilConstructor.prototype instanceof ShapeUtil + ) { + assert( + shapeUtil instanceof (shapeUtilConstructor as any), + `Shape util found for type "${shapeUtilConstructor.type}" is not an instance of the provided constructor` + ) + } + + return shapeUtil as T } /** diff --git a/packages/editor/src/lib/editor/shapeutils/ArrowShapeUtil/ArrowShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/ArrowShapeUtil/ArrowShapeUtil.tsx index af516d977..99e4ecb08 100644 --- a/packages/editor/src/lib/editor/shapeutils/ArrowShapeUtil/ArrowShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/ArrowShapeUtil/ArrowShapeUtil.tsx @@ -57,7 +57,7 @@ let globalRenderIndex = 0 /** @public */ export class ArrowShapeUtil extends ShapeUtil { - static override type = 'arrow' + static override type = 'arrow' as const override canEdit = () => true override canBind = () => false diff --git a/packages/editor/src/lib/editor/shapeutils/BookmarkShapeUtil/BookmarkShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/BookmarkShapeUtil/BookmarkShapeUtil.tsx index 559c9f545..01453e4ec 100644 --- a/packages/editor/src/lib/editor/shapeutils/BookmarkShapeUtil/BookmarkShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/BookmarkShapeUtil/BookmarkShapeUtil.tsx @@ -18,7 +18,7 @@ import { HyperlinkButton } from '../shared/HyperlinkButton' /** @public */ export class BookmarkShapeUtil extends BaseBoxShapeUtil { - static override type = 'bookmark' + static override type = 'bookmark' as const override canResize = () => false diff --git a/packages/editor/src/lib/editor/shapeutils/DrawShapeUtil/DrawShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/DrawShapeUtil/DrawShapeUtil.tsx index 05d7e5814..02a685319 100644 --- a/packages/editor/src/lib/editor/shapeutils/DrawShapeUtil/DrawShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/DrawShapeUtil/DrawShapeUtil.tsx @@ -22,7 +22,7 @@ import { getDrawShapeStrokeDashArray, getFreehandOptions, getPointsFromSegments /** @public */ export class DrawShapeUtil extends ShapeUtil { - static override type = 'draw' + static override type = 'draw' as const hideResizeHandles = (shape: TLDrawShape) => getIsDot(shape) hideRotateHandle = (shape: TLDrawShape) => getIsDot(shape) diff --git a/packages/editor/src/lib/editor/shapeutils/EmbedShapeUtil/EmbedShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/EmbedShapeUtil/EmbedShapeUtil.tsx index 3439a9c43..9d8b5914b 100644 --- a/packages/editor/src/lib/editor/shapeutils/EmbedShapeUtil/EmbedShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/EmbedShapeUtil/EmbedShapeUtil.tsx @@ -27,7 +27,7 @@ const getSandboxPermissions = (permissions: TLEmbedShapePermissions) => { /** @public */ export class EmbedShapeUtil extends BaseBoxShapeUtil { - static override type = 'embed' + static override type = 'embed' as const override canUnmount: TLShapeUtilFlag = () => false override canResize = (shape: TLEmbedShape) => { diff --git a/packages/editor/src/lib/editor/shapeutils/FrameShapeUtil/FrameShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/FrameShapeUtil/FrameShapeUtil.tsx index d4bf76ab3..a8f049b1e 100644 --- a/packages/editor/src/lib/editor/shapeutils/FrameShapeUtil/FrameShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/FrameShapeUtil/FrameShapeUtil.tsx @@ -11,7 +11,7 @@ import { FrameHeading } from './components/FrameHeading' /** @public */ export class FrameShapeUtil extends BaseBoxShapeUtil { - static override type = 'frame' + static override type = 'frame' as const override canBind = () => true diff --git a/packages/editor/src/lib/editor/shapeutils/GeoShapeUtil/GeoShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/GeoShapeUtil/GeoShapeUtil.tsx index a1deda784..23288ab3f 100644 --- a/packages/editor/src/lib/editor/shapeutils/GeoShapeUtil/GeoShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/GeoShapeUtil/GeoShapeUtil.tsx @@ -41,7 +41,7 @@ const MIN_SIZE_WITH_LABEL = 17 * 3 /** @public */ export class GeoShapeUtil extends BaseBoxShapeUtil { - static override type = 'geo' + static override type = 'geo' as const canEdit = () => true diff --git a/packages/editor/src/lib/editor/shapeutils/GroupShapeUtil/GroupShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/GroupShapeUtil/GroupShapeUtil.tsx index 223992286..94a12028d 100644 --- a/packages/editor/src/lib/editor/shapeutils/GroupShapeUtil/GroupShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/GroupShapeUtil/GroupShapeUtil.tsx @@ -6,7 +6,7 @@ import { DashedOutlineBox } from '../shared/DashedOutlineBox' /** @public */ export class GroupShapeUtil extends ShapeUtil { - static override type = 'group' + static override type = 'group' as const type = 'group' as const diff --git a/packages/editor/src/lib/editor/shapeutils/HighlightShapeUtil/HighlightShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/HighlightShapeUtil/HighlightShapeUtil.tsx index daa78a3ea..f9b7c87d6 100644 --- a/packages/editor/src/lib/editor/shapeutils/HighlightShapeUtil/HighlightShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/HighlightShapeUtil/HighlightShapeUtil.tsx @@ -15,7 +15,7 @@ const UNDERLAY_OPACITY = 0.82 /** @public */ export class HighlightShapeUtil extends ShapeUtil { - static type = 'highlight' + static type = 'highlight' as const hideResizeHandles = (shape: TLHighlightShape) => getIsDot(shape) hideRotateHandle = (shape: TLHighlightShape) => getIsDot(shape) diff --git a/packages/editor/src/lib/editor/shapeutils/ImageShapeUtil/ImageShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/ImageShapeUtil/ImageShapeUtil.tsx index 9654130f4..22683fdeb 100644 --- a/packages/editor/src/lib/editor/shapeutils/ImageShapeUtil/ImageShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/ImageShapeUtil/ImageShapeUtil.tsx @@ -49,7 +49,7 @@ async function getDataURIFromURL(url: string): Promise { /** @public */ export class ImageShapeUtil extends BaseBoxShapeUtil { - static override type = 'image' + static override type = 'image' as const override isAspectRatioLocked = () => true override canCrop = () => true diff --git a/packages/editor/src/lib/editor/shapeutils/LineShapeUtil/LineShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/LineShapeUtil/LineShapeUtil.tsx index c634a0741..1dd63ecc0 100644 --- a/packages/editor/src/lib/editor/shapeutils/LineShapeUtil/LineShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/LineShapeUtil/LineShapeUtil.tsx @@ -26,7 +26,7 @@ const handlesCache = new WeakMapCache() /** @public */ export class LineShapeUtil extends ShapeUtil { - static override type = 'line' + static override type = 'line' as const override hideResizeHandles = () => true override hideRotateHandle = () => true diff --git a/packages/editor/src/lib/editor/shapeutils/NoteShapeUtil/NoteShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/NoteShapeUtil/NoteShapeUtil.tsx index 6f07b1b39..c1c2d67df 100644 --- a/packages/editor/src/lib/editor/shapeutils/NoteShapeUtil/NoteShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/NoteShapeUtil/NoteShapeUtil.tsx @@ -12,7 +12,7 @@ const NOTE_SIZE = 200 /** @public */ export class NoteShapeUtil extends ShapeUtil { - static override type = 'note' + static override type = 'note' as const canEdit = () => true hideResizeHandles = () => true diff --git a/packages/editor/src/lib/editor/shapeutils/TextShapeUtil/TextShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/TextShapeUtil/TextShapeUtil.tsx index e380a4d90..f98b8ccb0 100644 --- a/packages/editor/src/lib/editor/shapeutils/TextShapeUtil/TextShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/TextShapeUtil/TextShapeUtil.tsx @@ -18,7 +18,7 @@ const sizeCache = new WeakMapCache { - static override type = 'text' + static override type = 'text' as const canEdit = () => true diff --git a/packages/editor/src/lib/editor/shapeutils/VideoShapeUtil/VideoShapeUtil.tsx b/packages/editor/src/lib/editor/shapeutils/VideoShapeUtil/VideoShapeUtil.tsx index 159a1d6e2..c1c5b787a 100644 --- a/packages/editor/src/lib/editor/shapeutils/VideoShapeUtil/VideoShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapeutils/VideoShapeUtil/VideoShapeUtil.tsx @@ -11,7 +11,7 @@ import { HyperlinkButton } from '../shared/HyperlinkButton' /** @public */ export class VideoShapeUtil extends BaseBoxShapeUtil { - static override type = 'video' + static override type = 'video' as const override canEdit = () => true override isAspectRatioLocked = () => true diff --git a/packages/editor/src/lib/hooks/useLocalStore.ts b/packages/editor/src/lib/hooks/useLocalStore.ts index 32a267c0d..46c837cf2 100644 --- a/packages/editor/src/lib/hooks/useLocalStore.ts +++ b/packages/editor/src/lib/hooks/useLocalStore.ts @@ -6,11 +6,11 @@ import { TLLocalSyncClient } from '../utils/sync/TLLocalSyncClient' import { useTLStore } from './useTLStore' /** @internal */ -export function useLocalStore( - opts = {} as { persistenceKey?: string; sessionId?: string } & TLStoreOptions -): TLStoreWithStatus { - const { persistenceKey, sessionId, ...rest } = opts - +export function useLocalStore({ + persistenceKey, + sessionId, + ...rest +}: { persistenceKey?: string; sessionId?: string } & TLStoreOptions): TLStoreWithStatus { const [state, setState] = useState<{ id: string; storeWithStatus: TLStoreWithStatus } | null>( null ) diff --git a/packages/editor/src/lib/test/Editor.test.tsx b/packages/editor/src/lib/test/Editor.test.tsx index f12622b58..ddaa28c3e 100644 --- a/packages/editor/src/lib/test/Editor.test.tsx +++ b/packages/editor/src/lib/test/Editor.test.tsx @@ -1,5 +1,7 @@ -import { PageRecordType, createShapeId } from '@tldraw/tlschema' +import { PageRecordType, TLShape, createShapeId } from '@tldraw/tlschema' import { structuredClone } from '@tldraw/utils' +import { BaseBoxShapeUtil } from '../editor/shapeutils/BaseBoxShapeUtil' +import { GeoShapeUtil } from '../editor/shapeutils/GeoShapeUtil/GeoShapeUtil' import { TestEditor } from './TestEditor' import { TL } from './jsx' @@ -441,3 +443,60 @@ describe('isFocused', () => { expect(blurMock).toHaveBeenCalled() }) }) + +describe('getShapeUtil', () => { + it('accepts shapes', () => { + const geoShape = editor.getShapeById(ids.box1)! + const geoUtil = editor.getShapeUtil(geoShape) + expect(geoUtil).toBeInstanceOf(GeoShapeUtil) + }) + + it('accepts shape utils', () => { + const geoUtil = editor.getShapeUtil(GeoShapeUtil) + expect(geoUtil).toBeInstanceOf(GeoShapeUtil) + }) + + it('throws if that shape type isnt registered', () => { + const myFakeShape = { type: 'fake' } as TLShape + expect(() => editor.getShapeUtil(myFakeShape)).toThrowErrorMatchingInlineSnapshot( + `"No shape util found for type \\"fake\\""` + ) + + class MyFakeShapeUtil extends BaseBoxShapeUtil { + static type = 'fake' + + defaultProps() { + throw new Error('Method not implemented.') + } + render() { + throw new Error('Method not implemented.') + } + indicator() { + throw new Error('Method not implemented.') + } + } + + expect(() => editor.getShapeUtil(MyFakeShapeUtil)).toThrowErrorMatchingInlineSnapshot( + `"No shape util found for type \\"fake\\""` + ) + }) + + it("throws if a shape util that isn't the one registered is passed in", () => { + class MyFakeGeoShapeUtil extends BaseBoxShapeUtil { + static type = 'geo' + + defaultProps() { + throw new Error('Method not implemented.') + } + render() { + throw new Error('Method not implemented.') + } + indicator() { + throw new Error('Method not implemented.') + } + } + expect(() => editor.getShapeUtil(MyFakeGeoShapeUtil)).toThrowErrorMatchingInlineSnapshot( + `"Shape util found for type \\"geo\\" is not an instance of the provided constructor"` + ) + }) +}) diff --git a/packages/editor/src/lib/test/TestEditor.ts b/packages/editor/src/lib/test/TestEditor.ts index 75d78fca7..165b61512 100644 --- a/packages/editor/src/lib/test/TestEditor.ts +++ b/packages/editor/src/lib/test/TestEditor.ts @@ -55,16 +55,14 @@ declare global { export const TEST_INSTANCE_ID = InstanceRecordType.createId('testInstance1') export class TestEditor extends Editor { - constructor(options = {} as Partial>) { + constructor(options: Partial> = {}) { const elm = document.createElement('div') - const { shapes = {}, tools = [] } = options + const { shapes = defaultShapes, tools = [] } = options elm.tabIndex = 0 super({ - shapes: { ...defaultShapes, ...shapes }, + shapes, tools: [...defaultTools, ...tools], - store: createTLStore({ - customShapes: shapes, - }), + store: createTLStore({ 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 f1983c4db..19159c996 100644 --- a/packages/editor/src/lib/test/TldrawEditor.test.tsx +++ b/packages/editor/src/lib/test/TldrawEditor.test.tsx @@ -1,9 +1,13 @@ import { act, render, screen } from '@testing-library/react' import { TLBaseShape, createShapeId } from '@tldraw/tlschema' +import { noop } from '@tldraw/utils' import { TldrawEditor } from '../TldrawEditor' import { Canvas } from '../components/Canvas' import { HTMLContainer } from '../components/HTMLContainer' import { createTLStore } from '../config/createTLStore' +import { defaultShapes } from '../config/defaultShapes' +import { defaultTools } from '../config/defaultTools' +import { defineShape } from '../config/defineShape' import { Editor } from '../editor/Editor' import { BaseBoxShapeUtil } from '../editor/shapeutils/BaseBoxShapeUtil' import { BaseBoxShapeTool } from '../editor/tools/BaseBoxShapeTool/BaseBoxShapeTool' @@ -23,56 +27,135 @@ afterEach(() => { window.fetch = originalFetch }) +function checkAllShapes(editor: Editor, shapes: string[]) { + expect(Object.keys(editor!.store.schema.types.shape.migrations.subTypeMigrations!)).toStrictEqual( + shapes + ) + + expect(Object.keys(editor!.shapeUtils)).toStrictEqual(shapes) +} + describe('', () => { it('Renders without crashing', async () => { - await act(async () => ( - + render( +
- )) - }) - - it('Creates its own store', async () => { - let store: any - render( - await act(async () => ( - { - store = editor.store - }} - autoFocus - > -
- - )) ) await screen.findByTestId('canvas-1') - expect(store).toBeTruthy() + }) + + it('Creates its own store with core shapes', async () => { + let editor: Editor + render( + { + editor = e + }} + autoFocus + > +
+ + ) + await screen.findByTestId('canvas-1') + checkAllShapes(editor!, ['group', 'embed', 'bookmark', 'image', 'text']) + }) + + it('Can be created with default shapes', async () => { + let editor: Editor + render( + { + editor = e + }} + autoFocus + > +
+ + ) + await screen.findByTestId('canvas-1') + expect(editor!).toBeTruthy() + + checkAllShapes(editor!, [ + 'group', + 'embed', + 'bookmark', + 'image', + 'text', + 'draw', + 'geo', + 'line', + 'note', + 'frame', + 'arrow', + 'highlight', + 'video', + ]) }) it('Renders with an external store', async () => { - const store = createTLStore() + const store = createTLStore({ shapes: [] }) render( - await act(async () => ( - { - expect(editor.store).toBe(store) - }} - autoFocus - > -
- - )) + { + expect(editor.store).toBe(store) + }} + autoFocus + > +
+ ) await screen.findByTestId('canvas-1') }) + it('throws if the store has different shapes to the ones passed in', async () => { + const spy = jest.spyOn(console, 'error').mockImplementation(noop) + expect(() => + render( + +
+ + ) + ).toThrowErrorMatchingInlineSnapshot( + `"Editor and store have different shapes: \\"draw\\" was passed into the editor but not the schema"` + ) + + expect(() => + render( + +
+ + ) + ).toThrowErrorMatchingInlineSnapshot( + `"Editor and store have different shapes: \\"draw\\" is present in the store schema but not provided to the editor"` + ) + spy.mockRestore() + }) + it('Accepts fresh versions of store and calls `onMount` for each one', async () => { - const initialStore = createTLStore({}) + const initialStore = createTLStore({ shapes: [] }) const onMount = jest.fn() const rendered = render( - +
) @@ -82,7 +165,12 @@ describe('', () => { expect(initialEditor.store).toBe(initialStore) // re-render with the same store: rendered.rerender( - +
) @@ -90,9 +178,14 @@ describe('', () => { // not called again: expect(onMount).toHaveBeenCalledTimes(1) // re-render with a new store: - const newStore = createTLStore({}) + const newStore = createTLStore({ shapes: [] }) rendered.rerender( - +
) @@ -105,17 +198,18 @@ describe('', () => { it('Renders the canvas and shapes', async () => { let editor = {} as Editor render( - await act(async () => ( - { - editor = editorApp - }} - > - -
- - )) + { + editor = editorApp + }} + > + +
+ ) await screen.findByTestId('canvas-1') @@ -220,24 +314,23 @@ describe('Custom shapes', () => { } const tools = [CardTool] - const shapes = { card: { util: CardUtil } } + const shapes = [defineShape('card', { util: CardUtil })] it('Uses custom shapes', async () => { let editor = {} as Editor render( - await act(async () => ( - { - editor = editorApp - }} - > - -
- - )) + { + editor = editorApp + }} + > + +
+ ) await screen.findByTestId('canvas-1') @@ -247,6 +340,7 @@ describe('Custom shapes', () => { }) expect(editor.shapeUtils.card).toBeTruthy() + checkAllShapes(editor, ['group', 'embed', 'bookmark', 'image', 'text', 'card']) const id = createShapeId() diff --git a/packages/editor/src/lib/test/tools/translating.test.ts b/packages/editor/src/lib/test/tools/translating.test.ts index 87a38f8ec..a6bb77f60 100644 --- a/packages/editor/src/lib/test/tools/translating.test.ts +++ b/packages/editor/src/lib/test/tools/translating.test.ts @@ -5,6 +5,7 @@ import { ShapeUtil } from '../../editor/shapeutils/ShapeUtil' import { TestEditor } from '../TestEditor' import { defaultShapes } from '../../config/defaultShapes' +import { defineShape } from '../../config/defineShape' import { getSnapLines } from '../testutils/getSnapLines' type __TopLeftSnapOnlyShape = any @@ -40,6 +41,10 @@ class __TopLeftSnapOnlyShapeUtil extends ShapeUtil<__TopLeftSnapOnlyShape> { } } +const __TopLeftSnapOnlyShape = defineShape('__test_top_left_snap_only', { + util: __TopLeftSnapOnlyShapeUtil, +}) + let editor: TestEditor afterEach(() => { @@ -753,12 +758,7 @@ describe('custom snapping points', () => { beforeEach(() => { editor?.dispose() editor = new TestEditor({ - shapes: { - ...defaultShapes, - __test_top_left_snap_only: { - util: __TopLeftSnapOnlyShapeUtil, - }, - }, + shapes: [...defaultShapes, __TopLeftSnapOnlyShape], // x───────┐ // │ T │ // │ │ diff --git a/packages/editor/src/lib/utils/sync/TLLocalSyncClient.test.ts b/packages/editor/src/lib/utils/sync/TLLocalSyncClient.test.ts index 84c3eef1f..dc03fa0a9 100644 --- a/packages/editor/src/lib/utils/sync/TLLocalSyncClient.test.ts +++ b/packages/editor/src/lib/utils/sync/TLLocalSyncClient.test.ts @@ -1,6 +1,7 @@ import { PageRecordType } from '@tldraw/tlschema' import { promiseWithResolve } from '@tldraw/utils' import { createTLStore } from '../../config/createTLStore' +import { defaultShapes } from '../../config/defaultShapes' import { TLLocalSyncClient } from './TLLocalSyncClient' import * as idb from './indexedDb' @@ -24,7 +25,7 @@ class BroadcastChannelMock { } function testClient(channel = new BroadcastChannelMock('test')) { - const store = createTLStore() + const store = createTLStore({ shapes: defaultShapes }) const onLoad = jest.fn(() => { return }) diff --git a/packages/editor/src/lib/utils/sync/indexedDb.test.ts b/packages/editor/src/lib/utils/sync/indexedDb.test.ts index df61a335e..fa32e9397 100644 --- a/packages/editor/src/lib/utils/sync/indexedDb.test.ts +++ b/packages/editor/src/lib/utils/sync/indexedDb.test.ts @@ -15,7 +15,7 @@ const clearAll = async () => { beforeEach(async () => { await clearAll() }) -const schema = createTLSchema() +const schema = createTLSchema({ shapes: {} }) describe('storeSnapshotInIndexedDb', () => { it("creates documents if they don't exist", async () => { await storeSnapshotInIndexedDb({ diff --git a/packages/file-format/api-report.md b/packages/file-format/api-report.md index 21703fbf4..aa962fb21 100644 --- a/packages/file-format/api-report.md +++ b/packages/file-format/api-report.md @@ -8,6 +8,7 @@ import { Editor } from '@tldraw/editor'; import { MigrationFailureReason } from '@tldraw/store'; import { Result } from '@tldraw/utils'; import { SerializedSchema } from '@tldraw/store'; +import { TLSchema } from '@tldraw/editor'; import { TLStore } from '@tldraw/editor'; import { TLUiToastsContextType } from '@tldraw/ui'; import { TLUiTranslationKey } from '@tldraw/ui'; @@ -39,8 +40,8 @@ export interface LegacyTldrawDocument { export function parseAndLoadDocument(editor: Editor, document: string, msg: (id: TLUiTranslationKey) => string, addToast: TLUiToastsContextType['addToast'], onV1FileLoad?: () => void, forceDarkMode?: boolean): Promise; // @public (undocumented) -export function parseTldrawJsonFile({ json, store, }: { - store: TLStore; +export function parseTldrawJsonFile({ json, schema, }: { + schema: TLSchema; json: string; }): Result; diff --git a/packages/file-format/src/lib/file.ts b/packages/file-format/src/lib/file.ts index 65fe2f15c..9ff98e789 100644 --- a/packages/file-format/src/lib/file.ts +++ b/packages/file-format/src/lib/file.ts @@ -5,6 +5,7 @@ import { TLAsset, TLAssetId, TLRecord, + TLSchema, TLStore, } from '@tldraw/editor' import { @@ -82,9 +83,9 @@ export type TldrawFileParseError = /** @public */ export function parseTldrawJsonFile({ json, - store, + schema, }: { - store: TLStore + schema: TLSchema json: string }): Result { // first off, we parse .json file and check it matches the general shape of @@ -121,7 +122,7 @@ export function parseTldrawJsonFile({ let migrationResult: MigrationResult> try { const storeSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r as TLRecord])) - migrationResult = store.schema.migrateStoreSnapshot(storeSnapshot, data.schema) + migrationResult = schema.migrateStoreSnapshot(storeSnapshot, data.schema) } catch (e) { // junk data in the migration return Result.err({ type: 'invalidRecords', cause: e }) @@ -138,6 +139,7 @@ export function parseTldrawJsonFile({ return Result.ok( createTLStore({ initialData: migrationResult.value, + schema, }) ) } catch (e) { @@ -216,7 +218,7 @@ export async function parseAndLoadDocument( forceDarkMode?: boolean ) { const parseFileResult = parseTldrawJsonFile({ - store: createTLStore(), + schema: editor.store.schema, json: document, }) if (!parseFileResult.ok) { diff --git a/packages/file-format/src/test/file.test.ts b/packages/file-format/src/test/file.test.ts index d8e191b9f..d0734da58 100644 --- a/packages/file-format/src/test/file.test.ts +++ b/packages/file-format/src/test/file.test.ts @@ -1,13 +1,11 @@ -import { createShapeId, createTLStore, TLStore } from '@tldraw/editor' +import { createShapeId, createTLStore, defaultShapes, TLStore } from '@tldraw/editor' import { MigrationFailureReason, UnknownRecord } from '@tldraw/store' import { assert } from '@tldraw/utils' import { parseTldrawJsonFile as _parseTldrawJsonFile, TldrawFile } from '../lib/file' -const parseTldrawJsonFile = (store: TLStore, json: string) => - _parseTldrawJsonFile({ - store, - json, - }) +const schema = createTLStore({ shapes: defaultShapes }).schema + +const parseTldrawJsonFile = (store: TLStore, json: string) => _parseTldrawJsonFile({ schema, json }) function serialize(file: TldrawFile): string { return JSON.stringify(file) @@ -15,21 +13,21 @@ function serialize(file: TldrawFile): string { describe('parseTldrawJsonFile', () => { it('returns an error if the file is not json', () => { - const store = createTLStore() + const store = createTLStore({ shapes: defaultShapes }) 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 store = createTLStore() + const store = createTLStore({ shapes: defaultShapes }) 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 store = createTLStore({ shapes: defaultShapes }) const result = parseTldrawJsonFile( store, serialize({ @@ -43,7 +41,7 @@ describe('parseTldrawJsonFile', () => { }) it('returns an error if the file version is too new', () => { - const store = createTLStore() + const store = createTLStore({ shapes: defaultShapes }) const result = parseTldrawJsonFile( store, serialize({ @@ -57,7 +55,7 @@ describe('parseTldrawJsonFile', () => { }) it('returns an error if migrations fail', () => { - const store = createTLStore() + const store = createTLStore({ shapes: defaultShapes }) const serializedSchema = store.schema.serialize() serializedSchema.storeVersion = 100 const result = parseTldrawJsonFile( @@ -72,7 +70,7 @@ describe('parseTldrawJsonFile', () => { assert(result.error.type === 'migrationFailed') expect(result.error.reason).toBe(MigrationFailureReason.TargetVersionTooOld) - const store2 = createTLStore() + const store2 = createTLStore({ shapes: defaultShapes }) const serializedSchema2 = store2.schema.serialize() serializedSchema2.recordVersions.shape.version = 100 const result2 = parseTldrawJsonFile( @@ -90,7 +88,7 @@ describe('parseTldrawJsonFile', () => { }) it('returns an error if a record is invalid', () => { - const store = createTLStore() + const store = createTLStore({ shapes: defaultShapes }) const result = parseTldrawJsonFile( store, serialize({ @@ -115,7 +113,7 @@ describe('parseTldrawJsonFile', () => { }) it('returns a store if the file is valid', () => { - const store = createTLStore() + const store = createTLStore({ shapes: defaultShapes }) const result = parseTldrawJsonFile( store, serialize({ diff --git a/packages/tldraw/src/lib/Tldraw.test.tsx b/packages/tldraw/src/lib/Tldraw.test.tsx index 9280ae9b6..816f5b2a6 100644 --- a/packages/tldraw/src/lib/Tldraw.test.tsx +++ b/packages/tldraw/src/lib/Tldraw.test.tsx @@ -1,5 +1,5 @@ import { act } from '@testing-library/react' -import { TldrawEditor } from '@tldraw/editor' +import { Tldraw } from './Tldraw' let originalFetch: typeof window.fetch beforeEach(() => { @@ -20,9 +20,9 @@ afterEach(() => { describe('', () => { it('Renders without crashing', async () => { await act(async () => ( - +
- + )) }) }) diff --git a/packages/tldraw/src/lib/Tldraw.tsx b/packages/tldraw/src/lib/Tldraw.tsx index 0ca054a18..511b44390 100644 --- a/packages/tldraw/src/lib/Tldraw.tsx +++ b/packages/tldraw/src/lib/Tldraw.tsx @@ -1,13 +1,26 @@ -import { Canvas, TldrawEditor, TldrawEditorProps } from '@tldraw/editor' +import { + Canvas, + TldrawEditor, + TldrawEditorProps, + defaultShapes, + defaultTools, +} from '@tldraw/editor' import { ContextMenu, TldrawUi, TldrawUiProps } from '@tldraw/ui' +import { useMemo } from 'react' /** @public */ export function Tldraw(props: TldrawEditorProps & TldrawUiProps) { const { children, ...rest } = props + const withDefaults = { + ...rest, + shapes: useMemo(() => [...defaultShapes, ...(rest.shapes ?? [])], [rest.shapes]), + tools: useMemo(() => [...defaultTools, ...(rest.tools ?? [])], [rest.tools]), + } + return ( - - + + diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index 751ecb19e..cea29af0f 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -18,6 +18,12 @@ import { UnknownRecord } from '@tldraw/store'; // @internal (undocumented) export const alignValidator: T.Validator<"end" | "middle" | "start">; +// @internal (undocumented) +export const arrowShapeMigrations: Migrations; + +// @internal (undocumented) +export const arrowShapeProps: ShapeProps; + // @public export const assetIdValidator: T.Validator; @@ -30,6 +36,12 @@ export const AssetRecordType: RecordType; // @internal (undocumented) export const assetValidator: T.Validator; +// @internal (undocumented) +export const bookmarkShapeMigrations: Migrations; + +// @internal (undocumented) +export const bookmarkShapeProps: ShapeProps; + // @public export interface Box2dModel { // (undocumented) @@ -45,12 +57,12 @@ export interface Box2dModel { // @public (undocumented) export const CameraRecordType: RecordType; +// @public +export const canvasUiColorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">; + // @internal (undocumented) export function CLIENT_FIXUP_SCRIPT(persistedStore: StoreSnapshot): StoreSnapshot; -// @public -export const colorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">; - // @internal (undocumented) export const colorValidator: T.Validator<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">; @@ -73,7 +85,9 @@ export const createPresenceStateDerivation: ($user: Signal<{ export function createShapeId(id?: string): TLShapeId; // @public (undocumented) -export function createShapeValidator(type: Type, props: T.Validator): T.ObjectValidator<{ +export function createShapeValidator(type: Type, props?: { + [K in keyof Props]: T.Validatable; +}): T.ObjectValidator<{ id: TLShapeId; typeName: "shape"; x: number; @@ -84,13 +98,13 @@ export function createShapeValidator( type: Type; isLocked: boolean; opacity: number; - props: Props; + props: Props | Record; }>; // @public -export function createTLSchema(opts?: { - customShapes: Record; -}): StoreSchema; +export function createTLSchema({ shapes }: { + shapes: Record; +}): TLSchema; // @internal (undocumented) export const dashValidator: T.Validator<"dashed" | "dotted" | "draw" | "solid">; @@ -98,6 +112,12 @@ export const dashValidator: T.Validator<"dashed" | "dotted" | "draw" | "solid">; // @public (undocumented) export const DocumentRecordType: RecordType; +// @internal (undocumented) +export const drawShapeMigrations: Migrations; + +// @internal (undocumented) +export const drawShapeProps: ShapeProps; + // @public (undocumented) export const EMBED_DEFINITIONS: readonly [{ readonly type: "tldraw"; @@ -285,6 +305,9 @@ export type EmbedDefinition = { readonly fromEmbedUrl: (url: string) => string | undefined; }; +// @internal (undocumented) +export const embedShapeMigrations: Migrations; + // @public export const embedShapePermissionDefaults: { readonly 'allow-downloads-without-user-activation': false; @@ -303,6 +326,9 @@ export const embedShapePermissionDefaults: { readonly 'allow-forms': true; }; +// @internal (undocumented) +export const embedShapeProps: ShapeProps; + // @internal (undocumented) export const fillValidator: T.Validator<"none" | "pattern" | "semi" | "solid">; @@ -315,18 +341,54 @@ export function fixupRecord(oldRecord: TLRecord): { // @internal (undocumented) export const fontValidator: T.Validator<"draw" | "mono" | "sans" | "serif">; +// @internal (undocumented) +export const frameShapeMigrations: Migrations; + +// @internal (undocumented) +export const frameShapeProps: ShapeProps; + +// @internal (undocumented) +export const geoShapeMigrations: Migrations; + +// @internal (undocumented) +export const geoShapeProps: ShapeProps; + // @internal (undocumented) export const geoValidator: T.Validator<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">; // @public (undocumented) export function getDefaultTranslationLocale(): TLLanguage['locale']; +// @internal (undocumented) +export const groupShapeMigrations: Migrations; + +// @internal (undocumented) +export const groupShapeProps: ShapeProps; + +// @internal (undocumented) +export const highlightShapeMigrations: Migrations; + +// @internal (undocumented) +export const highlightShapeProps: ShapeProps; + +// @internal (undocumented) +export const iconShapeMigrations: Migrations; + +// @internal (undocumented) +export const iconShapeProps: ShapeProps; + // @internal (undocumented) export const iconValidator: T.Validator<"activity" | "airplay" | "alert-circle" | "alert-octagon" | "alert-triangle" | "align-center" | "align-justify" | "align-left" | "align-right" | "anchor" | "aperture" | "archive" | "arrow-down-circle" | "arrow-down-left" | "arrow-down-right" | "arrow-down" | "arrow-left-circle" | "arrow-left" | "arrow-right-circle" | "arrow-right" | "arrow-up-circle" | "arrow-up-left" | "arrow-up-right" | "arrow-up" | "at-sign" | "award" | "bar-chart-2" | "bar-chart" | "battery-charging" | "battery" | "bell-off" | "bell" | "bluetooth" | "bold" | "book-open" | "book" | "bookmark" | "briefcase" | "calendar" | "camera-off" | "camera" | "cast" | "check-circle" | "check-square" | "check" | "chevron-down" | "chevron-left" | "chevron-right" | "chevron-up" | "chevrons-down" | "chevrons-left" | "chevrons-right" | "chevrons-up" | "chrome" | "circle" | "clipboard" | "clock" | "cloud-drizzle" | "cloud-lightning" | "cloud-off" | "cloud-rain" | "cloud-snow" | "cloud" | "codepen" | "codesandbox" | "coffee" | "columns" | "command" | "compass" | "copy" | "corner-down-left" | "corner-down-right" | "corner-left-down" | "corner-left-up" | "corner-right-down" | "corner-right-up" | "corner-up-left" | "corner-up-right" | "cpu" | "credit-card" | "crop" | "crosshair" | "database" | "delete" | "disc" | "divide-circle" | "divide-square" | "divide" | "dollar-sign" | "download-cloud" | "download" | "dribbble" | "droplet" | "edit-2" | "edit-3" | "edit" | "external-link" | "eye-off" | "eye" | "facebook" | "fast-forward" | "feather" | "figma" | "file-minus" | "file-plus" | "file-text" | "file" | "film" | "filter" | "flag" | "folder-minus" | "folder-plus" | "folder" | "framer" | "frown" | "geo" | "gift" | "git-branch" | "git-commit" | "git-merge" | "git-pull-request" | "github" | "gitlab" | "globe" | "grid" | "hard-drive" | "hash" | "headphones" | "heart" | "help-circle" | "hexagon" | "home" | "image" | "inbox" | "info" | "instagram" | "italic" | "key" | "layers" | "layout" | "life-buoy" | "link-2" | "link" | "linkedin" | "list" | "loader" | "lock" | "log-in" | "log-out" | "mail" | "map-pin" | "map" | "maximize-2" | "maximize" | "meh" | "menu" | "message-circle" | "message-square" | "mic-off" | "mic" | "minimize-2" | "minimize" | "minus-circle" | "minus-square" | "minus" | "monitor" | "moon" | "more-horizontal" | "more-vertical" | "mouse-pointer" | "move" | "music" | "navigation-2" | "navigation" | "octagon" | "package" | "paperclip" | "pause-circle" | "pause" | "pen-tool" | "percent" | "phone-call" | "phone-forwarded" | "phone-incoming" | "phone-missed" | "phone-off" | "phone-outgoing" | "phone" | "pie-chart" | "play-circle" | "play" | "plus-circle" | "plus-square" | "plus" | "pocket" | "power" | "printer" | "radio" | "refresh-ccw" | "refresh-cw" | "repeat" | "rewind" | "rotate-ccw" | "rotate-cw" | "rss" | "save" | "scissors" | "search" | "send" | "server" | "settings" | "share-2" | "share" | "shield-off" | "shield" | "shopping-bag" | "shopping-cart" | "shuffle" | "sidebar" | "skip-back" | "skip-forward" | "slack" | "slash" | "sliders" | "smartphone" | "smile" | "speaker" | "square" | "star" | "stop-circle" | "sun" | "sunrise" | "sunset" | "table" | "tablet" | "tag" | "target" | "terminal" | "thermometer" | "thumbs-down" | "thumbs-up" | "toggle-left" | "toggle-right" | "tool" | "trash-2" | "trash" | "trello" | "trending-down" | "trending-up" | "triangle" | "truck" | "tv" | "twitch" | "twitter" | "type" | "umbrella" | "underline" | "unlock" | "upload-cloud" | "upload" | "user-check" | "user-minus" | "user-plus" | "user-x" | "user" | "users" | "video-off" | "video" | "voicemail" | "volume-1" | "volume-2" | "volume-x" | "volume" | "watch" | "wifi-off" | "wifi" | "wind" | "x-circle" | "x-octagon" | "x-square" | "x" | "youtube" | "zap-off" | "zap" | "zoom-in" | "zoom-out">; // @internal (undocumented) export function idValidator>(prefix: Id['__type__']['typeName']): T.Validator; +// @internal (undocumented) +export const imageShapeMigrations: Migrations; + +// @internal (undocumented) +export const imageShapeProps: ShapeProps; + // @public (undocumented) export const InstancePageStateRecordType: RecordType; @@ -450,6 +512,18 @@ export const LANGUAGES: readonly [{ readonly label: "繁體中文 (台灣)"; }]; +// @internal (undocumented) +export const lineShapeMigrations: Migrations; + +// @internal (undocumented) +export const lineShapeProps: ShapeProps; + +// @internal (undocumented) +export const noteShapeMigrations: Migrations; + +// @internal (undocumented) +export const noteShapeProps: ShapeProps; + // @internal (undocumented) export const opacityValidator: T.Validator; @@ -468,18 +542,37 @@ export const PointerRecordType: RecordType; // @internal (undocumented) export const rootShapeMigrations: Migrations; +// @public (undocumented) +export type SchemaShapeInfo = { + migrations?: Migrations; + props?: Record any; + }>; +}; + // @internal (undocumented) export const scribbleValidator: T.Validator; // @public (undocumented) export const shapeIdValidator: T.Validator; +// @public (undocumented) +export type ShapeProps> = { + [K in keyof Shape['props']]: T.Validator; +}; + // @internal (undocumented) export const sizeValidator: T.Validator<"l" | "m" | "s" | "xl">; // @internal (undocumented) export const splineValidator: T.Validator<"cubic" | "line">; +// @internal (undocumented) +export const textShapeMigrations: Migrations; + +// @internal (undocumented) +export const textShapeProps: ShapeProps; + // @public (undocumented) export const TL_ALIGN_TYPES: Set<"end" | "middle" | "start">; @@ -487,7 +580,10 @@ export const TL_ALIGN_TYPES: Set<"end" | "middle" | "start">; export const TL_ARROWHEAD_TYPES: Set<"arrow" | "bar" | "diamond" | "dot" | "inverted" | "none" | "pipe" | "square" | "triangle">; // @public -export const TL_COLOR_TYPES: Set<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">; +export const TL_CANVAS_UI_COLOR_TYPES: Set<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">; + +// @public (undocumented) +export const TL_COLOR_TYPES: Set<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">; // @public (undocumented) export const TL_DASH_TYPES: Set<"dashed" | "dotted" | "draw" | "solid">; @@ -649,7 +745,7 @@ export interface TLCamera extends BaseRecord<'camera', TLCameraId> { export type TLCameraId = RecordId; // @public -export type TLColor = SetValue; +export type TLCanvasUiColor = SetValue; // @public (undocumented) export interface TLColorStyle extends TLBaseStyle { @@ -660,12 +756,12 @@ export interface TLColorStyle extends TLBaseStyle { } // @public (undocumented) -export type TLColorType = SetValue; +export type TLColorType = SetValue; // @public export interface TLCursor { // (undocumented) - color: TLColor; + color: TLCanvasUiColor; // (undocumented) rotation: number; // (undocumented) @@ -960,11 +1056,14 @@ export const TLPOINTER_ID: TLPointerId; // @public (undocumented) export type TLRecord = TLAsset | TLCamera | TLDocument | TLInstance | TLInstancePageState | TLInstancePresence | TLPage | TLPointer | TLShape; +// @public (undocumented) +export type TLSchema = StoreSchema; + // @public export type TLScribble = { points: Vec2dModel[]; size: number; - color: TLColor; + color: TLCanvasUiColor; opacity: number; state: SetValue; delay: number; @@ -1107,6 +1206,12 @@ export interface Vec2dModel { // @internal (undocumented) export const verticalAlignValidator: T.Validator<"end" | "middle" | "start">; +// @internal (undocumented) +export const videoShapeMigrations: Migrations; + +// @internal (undocumented) +export const videoShapeProps: ShapeProps; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/tlschema/src/createTLSchema.ts b/packages/tlschema/src/createTLSchema.ts index d57d4333d..1c1b36007 100644 --- a/packages/tlschema/src/createTLSchema.ts +++ b/packages/tlschema/src/createTLSchema.ts @@ -1,4 +1,5 @@ import { Migrations, StoreSchema, createRecordType, defineMigrations } from '@tldraw/store' +import { mapObjectMapValues } from '@tldraw/utils' import { T } from '@tldraw/validate' import { TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLStore' import { AssetRecordType } from './records/TLAsset' @@ -11,84 +12,17 @@ import { PointerRecordType } from './records/TLPointer' import { InstancePresenceRecordType } from './records/TLPresence' import { TLRecord } from './records/TLRecord' import { TLShape, rootShapeMigrations } from './records/TLShape' -import { arrowShapeMigrations, arrowShapeValidator } from './shapes/TLArrowShape' -import { bookmarkShapeMigrations, bookmarkShapeValidator } from './shapes/TLBookmarkShape' -import { drawShapeMigrations, drawShapeValidator } from './shapes/TLDrawShape' -import { embedShapeMigrations, embedShapeTypeValidator } from './shapes/TLEmbedShape' -import { frameShapeMigrations, frameShapeValidator } from './shapes/TLFrameShape' -import { geoShapeMigrations, geoShapeValidator } from './shapes/TLGeoShape' -import { groupShapeMigrations, groupShapeValidator } from './shapes/TLGroupShape' -import { highlightShapeMigrations, highlightShapeValidator } from './shapes/TLHighlightShape' -import { imageShapeMigrations, imageShapeValidator } from './shapes/TLImageShape' -import { lineShapeMigrations, lineShapeValidator } from './shapes/TLLineShape' -import { noteShapeMigrations, noteShapeValidator } from './shapes/TLNoteShape' -import { textShapeMigrations, textShapeValidator } from './shapes/TLTextShape' -import { videoShapeMigrations, videoShapeValidator } from './shapes/TLVideoShape' +import { createShapeValidator } from './shapes/TLBaseShape' import { storeMigrations } from './store-migrations' /** @public */ export type SchemaShapeInfo = { migrations?: Migrations - validator?: { validate: (record: any) => any } + props?: Record any }> } -const coreShapes: Record = { - group: { - migrations: groupShapeMigrations, - validator: groupShapeValidator, - }, - bookmark: { - migrations: bookmarkShapeMigrations, - validator: bookmarkShapeValidator, - }, - embed: { - migrations: embedShapeMigrations, - validator: embedShapeTypeValidator, - }, - image: { - migrations: imageShapeMigrations, - validator: imageShapeValidator, - }, - text: { - migrations: textShapeMigrations, - validator: textShapeValidator, - }, - video: { - migrations: videoShapeMigrations, - validator: videoShapeValidator, - }, -} - -const defaultShapes: Record = { - arrow: { - migrations: arrowShapeMigrations, - validator: arrowShapeValidator, - }, - draw: { - migrations: drawShapeMigrations, - validator: drawShapeValidator, - }, - frame: { - migrations: frameShapeMigrations, - validator: frameShapeValidator, - }, - geo: { - migrations: geoShapeMigrations, - validator: geoShapeValidator, - }, - line: { - migrations: lineShapeMigrations, - validator: lineShapeValidator, - }, - note: { - migrations: noteShapeMigrations, - validator: noteShapeValidator, - }, - highlight: { - migrations: highlightShapeMigrations, - validator: highlightShapeValidator, - }, -} +/** @public */ +export type TLSchema = StoreSchema /** * Create a TLSchema with custom shapes. Custom shapes cannot override default shapes. @@ -96,45 +30,26 @@ const defaultShapes: Record = { * @param opts - Options * * @public */ -export function createTLSchema( - opts = {} as { - customShapes: Record - } -) { - const { customShapes } = opts - - for (const key in customShapes) { - if (key in coreShapes) { - throw Error(`Can't override default shape ${key}!`) - } - } - - const allShapeEntries = Object.entries({ ...coreShapes, ...defaultShapes, ...customShapes }) - +export function createTLSchema({ shapes }: { shapes: Record }): TLSchema { const ShapeRecordType = createRecordType('shape', { migrations: defineMigrations({ currentVersion: rootShapeMigrations.currentVersion, firstVersion: rootShapeMigrations.firstVersion, migrators: rootShapeMigrations.migrators, subTypeKey: 'type', - subTypeMigrations: { - ...Object.fromEntries( - allShapeEntries.map(([k, v]) => [k, v.migrations ?? defineMigrations({})]) - ), - }, + subTypeMigrations: mapObjectMapValues(shapes, (k, v) => v.migrations ?? defineMigrations({})), }), scope: 'document', validator: T.model( 'shape', - T.union('type', { - ...Object.fromEntries( - allShapeEntries.map(([k, v]) => [k, (v.validator as T.Validator) ?? T.any]) - ), - }) + T.union( + 'type', + mapObjectMapValues(shapes, (type, { props }) => createShapeValidator(type, props)) + ) ), }).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false, opacity: 1 })) - return StoreSchema.create( + return StoreSchema.create( { asset: AssetRecordType, camera: CameraRecordType, diff --git a/packages/tlschema/src/index.ts b/packages/tlschema/src/index.ts index 16f4c630c..eb554f5e8 100644 --- a/packages/tlschema/src/index.ts +++ b/packages/tlschema/src/index.ts @@ -9,9 +9,13 @@ export { type TLBookmarkAsset } from './assets/TLBookmarkAsset' export { type TLImageAsset } from './assets/TLImageAsset' export { type TLVideoAsset } from './assets/TLVideoAsset' export { createPresenceStateDerivation } from './createPresenceStateDerivation' -export { createTLSchema } from './createTLSchema' +export { createTLSchema, type SchemaShapeInfo, type TLSchema } from './createTLSchema' export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup' -export { TL_COLOR_TYPES, colorTypeValidator, type TLColor } from './misc/TLColor' +export { + TL_CANVAS_UI_COLOR_TYPES, + canvasUiColorTypeValidator, + type TLCanvasUiColor, +} from './misc/TLColor' export { type TLCursor, type TLCursorType } from './misc/TLCursor' export { type TLHandle, type TLHandleType } from './misc/TLHandle' export { scribbleValidator, type TLScribble } from './misc/TLScribble' @@ -63,6 +67,8 @@ export { type TLUnknownShape, } from './records/TLShape' export { + arrowShapeMigrations, + arrowShapeProps, type TLArrowShape, type TLArrowShapeProps, type TLArrowTerminal, @@ -72,27 +78,54 @@ export { createShapeValidator, parentIdValidator, shapeIdValidator, + type ShapeProps, type TLBaseShape, } from './shapes/TLBaseShape' -export { type TLBookmarkShape } from './shapes/TLBookmarkShape' -export { type TLDrawShape, type TLDrawShapeSegment } from './shapes/TLDrawShape' +export { + bookmarkShapeMigrations, + bookmarkShapeProps, + type TLBookmarkShape, +} from './shapes/TLBookmarkShape' +export { + drawShapeMigrations, + drawShapeProps, + type TLDrawShape, + type TLDrawShapeSegment, +} from './shapes/TLDrawShape' export { EMBED_DEFINITIONS, + embedShapeMigrations, embedShapePermissionDefaults, + embedShapeProps, type EmbedDefinition, type TLEmbedShape, type TLEmbedShapePermissions, } from './shapes/TLEmbedShape' -export { type TLFrameShape } from './shapes/TLFrameShape' -export { type TLGeoShape } from './shapes/TLGeoShape' -export { type TLGroupShape } from './shapes/TLGroupShape' -export { type TLHighlightShape } from './shapes/TLHighlightShape' -export { type TLIconShape } from './shapes/TLIconShape' -export { type TLImageCrop, type TLImageShape, type TLImageShapeProps } from './shapes/TLImageShape' -export { type TLLineShape } from './shapes/TLLineShape' -export { type TLNoteShape } from './shapes/TLNoteShape' -export { type TLTextShape, type TLTextShapeProps } from './shapes/TLTextShape' -export { type TLVideoShape } from './shapes/TLVideoShape' +export { frameShapeMigrations, frameShapeProps, type TLFrameShape } from './shapes/TLFrameShape' +export { geoShapeMigrations, geoShapeProps, type TLGeoShape } from './shapes/TLGeoShape' +export { groupShapeMigrations, groupShapeProps, type TLGroupShape } from './shapes/TLGroupShape' +export { + highlightShapeMigrations, + highlightShapeProps, + type TLHighlightShape, +} from './shapes/TLHighlightShape' +export { iconShapeMigrations, iconShapeProps, type TLIconShape } from './shapes/TLIconShape' +export { + imageShapeMigrations, + imageShapeProps, + type TLImageCrop, + type TLImageShape, + type TLImageShapeProps, +} from './shapes/TLImageShape' +export { lineShapeMigrations, lineShapeProps, type TLLineShape } from './shapes/TLLineShape' +export { noteShapeMigrations, noteShapeProps, type TLNoteShape } from './shapes/TLNoteShape' +export { + textShapeMigrations, + textShapeProps, + type TLTextShape, + type TLTextShapeProps, +} from './shapes/TLTextShape' +export { videoShapeMigrations, videoShapeProps, type TLVideoShape } from './shapes/TLVideoShape' export { TL_ALIGN_TYPES, alignValidator, @@ -106,7 +139,12 @@ export { type TLArrowheadType, } from './styles/TLArrowheadStyle' export { TL_STYLE_TYPES, type TLStyleType } from './styles/TLBaseStyle' -export { colorValidator, type TLColorStyle, type TLColorType } from './styles/TLColorStyle' +export { + TL_COLOR_TYPES, + colorValidator, + type TLColorStyle, + type TLColorType, +} from './styles/TLColorStyle' export { TL_DASH_TYPES, dashValidator, diff --git a/packages/tlschema/src/misc/TLColor.ts b/packages/tlschema/src/misc/TLColor.ts index 6d4487f88..810b710b1 100644 --- a/packages/tlschema/src/misc/TLColor.ts +++ b/packages/tlschema/src/misc/TLColor.ts @@ -5,7 +5,7 @@ import { SetValue } from '../util-types' * The colors used by tldraw's default shapes. * * @public */ -export const TL_COLOR_TYPES = new Set([ +export const TL_CANVAS_UI_COLOR_TYPES = new Set([ 'accent', 'white', 'black', @@ -19,10 +19,10 @@ export const TL_COLOR_TYPES = new Set([ * A type for the colors used by tldraw's default shapes. * * @public */ -export type TLColor = SetValue +export type TLCanvasUiColor = SetValue /** * A validator for the colors used by tldraw's default shapes. * * @public */ -export const colorTypeValidator = T.setEnum(TL_COLOR_TYPES) +export const canvasUiColorTypeValidator = T.setEnum(TL_CANVAS_UI_COLOR_TYPES) diff --git a/packages/tlschema/src/misc/TLCursor.ts b/packages/tlschema/src/misc/TLCursor.ts index b3d1ae4d8..d7990ff89 100644 --- a/packages/tlschema/src/misc/TLCursor.ts +++ b/packages/tlschema/src/misc/TLCursor.ts @@ -1,6 +1,6 @@ import { T } from '@tldraw/validate' import { SetValue } from '../util-types' -import { TLColor, colorTypeValidator } from './TLColor' +import { TLCanvasUiColor, canvasUiColorTypeValidator } from './TLColor' /** * The cursor types used by tldraw's default shapes. @@ -44,14 +44,14 @@ export const cursorTypeValidator = T.setEnum(TL_CURSOR_TYPES) * * @public */ export interface TLCursor { - color: TLColor + color: TLCanvasUiColor type: TLCursorType rotation: number } /** @internal */ export const cursorValidator: T.Validator = T.object({ - color: colorTypeValidator, + color: canvasUiColorTypeValidator, type: cursorTypeValidator, rotation: T.number, }) diff --git a/packages/tlschema/src/misc/TLScribble.ts b/packages/tlschema/src/misc/TLScribble.ts index 3f53c91be..48a5cac0f 100644 --- a/packages/tlschema/src/misc/TLScribble.ts +++ b/packages/tlschema/src/misc/TLScribble.ts @@ -1,6 +1,6 @@ import { T } from '@tldraw/validate' import { SetValue } from '../util-types' -import { TLColor, colorTypeValidator } from './TLColor' +import { TLCanvasUiColor, canvasUiColorTypeValidator } from './TLColor' import { Vec2dModel } from './geometry-types' /** @@ -16,7 +16,7 @@ export const TL_SCRIBBLE_STATES = new Set(['starting', 'paused', 'active', 'stop export type TLScribble = { points: Vec2dModel[] size: number - color: TLColor + color: TLCanvasUiColor opacity: number state: SetValue delay: number @@ -26,7 +26,7 @@ export type TLScribble = { export const scribbleValidator: T.Validator = T.object({ points: T.arrayOf(T.point), size: T.positiveNumber, - color: colorTypeValidator, + color: canvasUiColorTypeValidator, opacity: T.number, state: T.setEnum(TL_SCRIBBLE_STATES), delay: T.number, diff --git a/packages/tlschema/src/shapes/TLArrowShape.ts b/packages/tlschema/src/shapes/TLArrowShape.ts index 60622224e..1a8411922 100644 --- a/packages/tlschema/src/shapes/TLArrowShape.ts +++ b/packages/tlschema/src/shapes/TLArrowShape.ts @@ -9,7 +9,7 @@ import { TLFillType, fillValidator } from '../styles/TLFillStyle' import { TLFontType, fontValidator } from '../styles/TLFontStyle' import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle' import { SetValue } from '../util-types' -import { TLBaseShape, createShapeValidator, shapeIdValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape, shapeIdValidator } from './TLBaseShape' /** @public */ export const TL_ARROW_TERMINAL_TYPE = new Set(['binding', 'point'] as const) @@ -62,23 +62,20 @@ export const arrowTerminalValidator: T.Validator = T.union('typ }) /** @internal */ -export const arrowShapeValidator: T.Validator = createShapeValidator( - 'arrow', - T.object({ - labelColor: colorValidator, - color: colorValidator, - fill: fillValidator, - dash: dashValidator, - size: sizeValidator, - arrowheadStart: arrowheadValidator, - arrowheadEnd: arrowheadValidator, - font: fontValidator, - start: arrowTerminalValidator, - end: arrowTerminalValidator, - bend: T.number, - text: T.string, - }) -) +export const arrowShapeProps: ShapeProps = { + labelColor: colorValidator, + color: colorValidator, + fill: fillValidator, + dash: dashValidator, + size: sizeValidator, + arrowheadStart: arrowheadValidator, + arrowheadEnd: arrowheadValidator, + font: fontValidator, + start: arrowTerminalValidator, + end: arrowTerminalValidator, + bend: T.number, + text: T.string, +} const Versions = { AddLabelColor: 1, diff --git a/packages/tlschema/src/shapes/TLBaseShape.ts b/packages/tlschema/src/shapes/TLBaseShape.ts index f378ddb30..550003fe6 100644 --- a/packages/tlschema/src/shapes/TLBaseShape.ts +++ b/packages/tlschema/src/shapes/TLBaseShape.ts @@ -32,7 +32,7 @@ export const shapeIdValidator = idValidator('shape') /** @public */ export function createShapeValidator( type: Type, - props: T.Validator + props?: { [K in keyof Props]: T.Validatable } ) { return T.object({ id: shapeIdValidator, @@ -45,6 +45,11 @@ export function createShapeValidator( type: T.literal(type), isLocked: T.boolean, opacity: opacityValidator, - props, + props: props ? T.object(props) : T.unknownObject, }) } + +/** @public */ +export type ShapeProps> = { + [K in keyof Shape['props']]: T.Validator +} diff --git a/packages/tlschema/src/shapes/TLBookmarkShape.ts b/packages/tlschema/src/shapes/TLBookmarkShape.ts index dc2bdfcb0..69952cb8c 100644 --- a/packages/tlschema/src/shapes/TLBookmarkShape.ts +++ b/packages/tlschema/src/shapes/TLBookmarkShape.ts @@ -2,7 +2,7 @@ import { defineMigrations } from '@tldraw/store' import { T } from '@tldraw/validate' import { assetIdValidator } from '../assets/TLBaseAsset' import { TLAssetId } from '../records/TLAsset' -import { TLBaseShape, createShapeValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' /** @public */ export type TLBookmarkShapeProps = { @@ -16,15 +16,12 @@ export type TLBookmarkShapeProps = { export type TLBookmarkShape = TLBaseShape<'bookmark', TLBookmarkShapeProps> /** @internal */ -export const bookmarkShapeValidator: T.Validator = createShapeValidator( - 'bookmark', - T.object({ - w: T.nonZeroNumber, - h: T.nonZeroNumber, - assetId: assetIdValidator.nullable(), - url: T.string, - }) -) +export const bookmarkShapeProps: ShapeProps = { + w: T.nonZeroNumber, + h: T.nonZeroNumber, + assetId: assetIdValidator.nullable(), + url: T.string, +} const Versions = { NullAssetId: 1, diff --git a/packages/tlschema/src/shapes/TLDrawShape.ts b/packages/tlschema/src/shapes/TLDrawShape.ts index a4b131eb2..d5705ac11 100644 --- a/packages/tlschema/src/shapes/TLDrawShape.ts +++ b/packages/tlschema/src/shapes/TLDrawShape.ts @@ -6,7 +6,7 @@ import { TLDashType, dashValidator } from '../styles/TLDashStyle' import { TLFillType, fillValidator } from '../styles/TLFillStyle' import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle' import { SetValue } from '../util-types' -import { TLBaseShape, createShapeValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' /** @public */ const TL_DRAW_SHAPE_SEGMENT_TYPE = new Set(['free', 'straight'] as const) @@ -39,19 +39,16 @@ export type TLDrawShapeProps = { export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps> /** @internal */ -export const drawShapeValidator: T.Validator = createShapeValidator( - 'draw', - T.object({ - color: colorValidator, - fill: fillValidator, - dash: dashValidator, - size: sizeValidator, - segments: T.arrayOf(drawShapeSegmentValidator), - isComplete: T.boolean, - isClosed: T.boolean, - isPen: T.boolean, - }) -) +export const drawShapeProps: ShapeProps = { + color: colorValidator, + fill: fillValidator, + dash: dashValidator, + size: sizeValidator, + segments: T.arrayOf(drawShapeSegmentValidator), + isComplete: T.boolean, + isClosed: T.boolean, + isPen: T.boolean, +} const Versions = { AddInPen: 1, diff --git a/packages/tlschema/src/shapes/TLEmbedShape.ts b/packages/tlschema/src/shapes/TLEmbedShape.ts index 8b198b8ff..60d823298 100644 --- a/packages/tlschema/src/shapes/TLEmbedShape.ts +++ b/packages/tlschema/src/shapes/TLEmbedShape.ts @@ -1,6 +1,6 @@ import { defineMigrations } from '@tldraw/store' import { T } from '@tldraw/validate' -import { TLBaseShape, createShapeValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' // Only allow multiplayer embeds. If we add additional routes later for example '/help' this won't match const TLDRAW_APP_RE = /(^\/r\/[^/]+\/?$)/ @@ -561,22 +561,19 @@ export type TLEmbedShapeProps = { export type TLEmbedShape = TLBaseShape<'embed', TLEmbedShapeProps> /** @internal */ -export const embedShapeTypeValidator: T.Validator = createShapeValidator( - 'embed', - T.object({ - w: T.nonZeroNumber, - h: T.nonZeroNumber, - url: T.string, - tmpOldUrl: T.string.optional(), - doesResize: T.boolean, - overridePermissions: T.dict( - T.setEnum( - new Set(Object.keys(embedShapePermissionDefaults) as (keyof TLEmbedShapePermissions)[]) - ), - T.boolean.optional() - ).optional(), - }) -) +export const embedShapeProps: ShapeProps = { + w: T.nonZeroNumber, + h: T.nonZeroNumber, + url: T.string, + tmpOldUrl: T.string.optional(), + doesResize: T.boolean, + overridePermissions: T.dict( + T.setEnum( + new Set(Object.keys(embedShapePermissionDefaults) as (keyof TLEmbedShapePermissions)[]) + ), + T.boolean.optional() + ).optional(), +} /** @public */ export type EmbedDefinition = { diff --git a/packages/tlschema/src/shapes/TLFrameShape.ts b/packages/tlschema/src/shapes/TLFrameShape.ts index 33ebc0bbd..1cce22bf7 100644 --- a/packages/tlschema/src/shapes/TLFrameShape.ts +++ b/packages/tlschema/src/shapes/TLFrameShape.ts @@ -1,6 +1,6 @@ import { defineMigrations } from '@tldraw/store' import { T } from '@tldraw/validate' -import { createShapeValidator, TLBaseShape } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' type TLFrameShapeProps = { w: number @@ -12,14 +12,11 @@ type TLFrameShapeProps = { export type TLFrameShape = TLBaseShape<'frame', TLFrameShapeProps> /** @internal */ -export const frameShapeValidator: T.Validator = createShapeValidator( - 'frame', - T.object({ - w: T.nonZeroNumber, - h: T.nonZeroNumber, - name: T.string, - }) -) +export const frameShapeProps: ShapeProps = { + w: T.nonZeroNumber, + h: T.nonZeroNumber, + name: T.string, +} /** @internal */ export const frameShapeMigrations = defineMigrations({}) diff --git a/packages/tlschema/src/shapes/TLGeoShape.ts b/packages/tlschema/src/shapes/TLGeoShape.ts index 70cc8ea29..5d1a647d0 100644 --- a/packages/tlschema/src/shapes/TLGeoShape.ts +++ b/packages/tlschema/src/shapes/TLGeoShape.ts @@ -8,7 +8,7 @@ import { TLFontType, fontValidator } from '../styles/TLFontStyle' import { TLGeoType, geoValidator } from '../styles/TLGeoStyle' import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle' import { TLVerticalAlignType, verticalAlignValidator } from '../styles/TLVerticalAlignStyle' -import { TLBaseShape, createShapeValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' /** @public */ export type TLGeoShapeProps = { @@ -32,25 +32,22 @@ export type TLGeoShapeProps = { export type TLGeoShape = TLBaseShape<'geo', TLGeoShapeProps> /** @internal */ -export const geoShapeValidator: T.Validator = createShapeValidator( - 'geo', - T.object({ - geo: geoValidator, - labelColor: colorValidator, - color: colorValidator, - fill: fillValidator, - dash: dashValidator, - size: sizeValidator, - font: fontValidator, - align: alignValidator, - verticalAlign: verticalAlignValidator, - url: T.string, - w: T.nonZeroNumber, - h: T.nonZeroNumber, - growY: T.positiveNumber, - text: T.string, - }) -) +export const geoShapeProps: ShapeProps = { + geo: geoValidator, + labelColor: colorValidator, + color: colorValidator, + fill: fillValidator, + dash: dashValidator, + size: sizeValidator, + font: fontValidator, + align: alignValidator, + verticalAlign: verticalAlignValidator, + url: T.string, + w: T.nonZeroNumber, + h: T.nonZeroNumber, + growY: T.positiveNumber, + text: T.string, +} const Versions = { AddUrlProp: 1, diff --git a/packages/tlschema/src/shapes/TLGroupShape.ts b/packages/tlschema/src/shapes/TLGroupShape.ts index 3558b7824..458d1cb13 100644 --- a/packages/tlschema/src/shapes/TLGroupShape.ts +++ b/packages/tlschema/src/shapes/TLGroupShape.ts @@ -1,6 +1,5 @@ import { defineMigrations } from '@tldraw/store' -import { T } from '@tldraw/validate' -import { createShapeValidator, TLBaseShape } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' /** @public */ export type TLGroupShapeProps = { [key in never]: undefined } @@ -9,10 +8,7 @@ export type TLGroupShapeProps = { [key in never]: undefined } export type TLGroupShape = TLBaseShape<'group', TLGroupShapeProps> /** @internal */ -export const groupShapeValidator: T.Validator = createShapeValidator( - 'group', - T.object({}) -) +export const groupShapeProps: ShapeProps = {} /** @internal */ export const groupShapeMigrations = defineMigrations({}) diff --git a/packages/tlschema/src/shapes/TLHighlightShape.ts b/packages/tlschema/src/shapes/TLHighlightShape.ts index 62069756f..00b7445a0 100644 --- a/packages/tlschema/src/shapes/TLHighlightShape.ts +++ b/packages/tlschema/src/shapes/TLHighlightShape.ts @@ -2,7 +2,7 @@ import { defineMigrations } from '@tldraw/store' import { T } from '@tldraw/validate' import { TLColorType, colorValidator } from '../styles/TLColorStyle' import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle' -import { TLBaseShape, createShapeValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' import { TLDrawShapeSegment, drawShapeSegmentValidator } from './TLDrawShape' /** @public */ @@ -18,16 +18,13 @@ export type TLHighlightShapeProps = { export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps> /** @internal */ -export const highlightShapeValidator: T.Validator = createShapeValidator( - 'highlight', - T.object({ - color: colorValidator, - size: sizeValidator, - segments: T.arrayOf(drawShapeSegmentValidator), - isComplete: T.boolean, - isPen: T.boolean, - }) -) +export const highlightShapeProps: ShapeProps = { + color: colorValidator, + size: sizeValidator, + segments: T.arrayOf(drawShapeSegmentValidator), + isComplete: T.boolean, + isPen: T.boolean, +} /** @internal */ export const highlightShapeMigrations = defineMigrations({}) diff --git a/packages/tlschema/src/shapes/TLIconShape.ts b/packages/tlschema/src/shapes/TLIconShape.ts index 79a355987..191ff0af8 100644 --- a/packages/tlschema/src/shapes/TLIconShape.ts +++ b/packages/tlschema/src/shapes/TLIconShape.ts @@ -4,7 +4,7 @@ import { TLColorType, colorValidator } from '../styles/TLColorStyle' import { TLDashType, dashValidator } from '../styles/TLDashStyle' import { TLIconType, iconValidator } from '../styles/TLIconStyle' import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle' -import { TLBaseShape, createShapeValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' /** @public */ export type TLIconShapeProps = { @@ -19,16 +19,13 @@ export type TLIconShapeProps = { export type TLIconShape = TLBaseShape<'icon', TLIconShapeProps> /** @internal */ -export const iconShapeValidator: T.Validator = createShapeValidator( - 'icon', - T.object({ - size: sizeValidator, - icon: iconValidator, - dash: dashValidator, - color: colorValidator, - scale: T.number, - }) -) +export const iconShapeProps: ShapeProps = { + size: sizeValidator, + icon: iconValidator, + dash: dashValidator, + color: colorValidator, + scale: T.number, +} /** @internal */ export const iconShapeMigrations = defineMigrations({}) diff --git a/packages/tlschema/src/shapes/TLImageShape.ts b/packages/tlschema/src/shapes/TLImageShape.ts index 1cca45e84..6ac472bb6 100644 --- a/packages/tlschema/src/shapes/TLImageShape.ts +++ b/packages/tlschema/src/shapes/TLImageShape.ts @@ -3,7 +3,7 @@ import { T } from '@tldraw/validate' import { assetIdValidator } from '../assets/TLBaseAsset' import { Vec2dModel } from '../misc/geometry-types' import { TLAssetId } from '../records/TLAsset' -import { TLBaseShape, createShapeValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' /** @public */ export type TLImageCrop = { @@ -30,17 +30,14 @@ export const cropValidator = T.object({ }) /** @internal */ -export const imageShapeValidator: T.Validator = createShapeValidator( - 'image', - T.object({ - w: T.nonZeroNumber, - h: T.nonZeroNumber, - playing: T.boolean, - url: T.string, - assetId: assetIdValidator.nullable(), - crop: cropValidator.nullable(), - }) -) +export const imageShapeProps: ShapeProps = { + w: T.nonZeroNumber, + h: T.nonZeroNumber, + playing: T.boolean, + url: T.string, + assetId: assetIdValidator.nullable(), + crop: cropValidator.nullable(), +} const Versions = { AddUrlProp: 1, diff --git a/packages/tlschema/src/shapes/TLLineShape.ts b/packages/tlschema/src/shapes/TLLineShape.ts index 63858d0d6..5226855eb 100644 --- a/packages/tlschema/src/shapes/TLLineShape.ts +++ b/packages/tlschema/src/shapes/TLLineShape.ts @@ -5,7 +5,7 @@ import { TLColorType, colorValidator } from '../styles/TLColorStyle' import { TLDashType, dashValidator } from '../styles/TLDashStyle' import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle' import { TLSplineType, splineValidator } from '../styles/TLSplineStyle' -import { TLBaseShape, createShapeValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' /** @public */ export type TLLineShapeProps = { @@ -22,16 +22,13 @@ export type TLLineShapeProps = { export type TLLineShape = TLBaseShape<'line', TLLineShapeProps> /** @internal */ -export const lineShapeValidator: T.Validator = createShapeValidator( - 'line', - T.object({ - color: colorValidator, - dash: dashValidator, - size: sizeValidator, - spline: splineValidator, - handles: T.dict(T.string, handleValidator), - }) -) +export const lineShapeProps: ShapeProps = { + color: colorValidator, + dash: dashValidator, + size: sizeValidator, + spline: splineValidator, + handles: T.dict(T.string, handleValidator), +} /** @internal */ export const lineShapeMigrations = defineMigrations({}) diff --git a/packages/tlschema/src/shapes/TLNoteShape.ts b/packages/tlschema/src/shapes/TLNoteShape.ts index c40a413ea..c0ba28358 100644 --- a/packages/tlschema/src/shapes/TLNoteShape.ts +++ b/packages/tlschema/src/shapes/TLNoteShape.ts @@ -5,7 +5,7 @@ import { TLColorType, colorValidator } from '../styles/TLColorStyle' import { TLFontType, fontValidator } from '../styles/TLFontStyle' import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle' import { TLVerticalAlignType, verticalAlignValidator } from '../styles/TLVerticalAlignStyle' -import { TLBaseShape, createShapeValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' /** @public */ export type TLNoteShapeProps = { @@ -23,19 +23,16 @@ export type TLNoteShapeProps = { export type TLNoteShape = TLBaseShape<'note', TLNoteShapeProps> /** @internal */ -export const noteShapeValidator: T.Validator = createShapeValidator( - 'note', - T.object({ - color: colorValidator, - size: sizeValidator, - font: fontValidator, - align: alignValidator, - verticalAlign: verticalAlignValidator, - growY: T.positiveNumber, - url: T.string, - text: T.string, - }) -) +export const noteShapeProps: ShapeProps = { + color: colorValidator, + size: sizeValidator, + font: fontValidator, + align: alignValidator, + verticalAlign: verticalAlignValidator, + growY: T.positiveNumber, + url: T.string, + text: T.string, +} const Versions = { AddUrlProp: 1, diff --git a/packages/tlschema/src/shapes/TLTextShape.ts b/packages/tlschema/src/shapes/TLTextShape.ts index 3aeb8a7ad..e25643e1b 100644 --- a/packages/tlschema/src/shapes/TLTextShape.ts +++ b/packages/tlschema/src/shapes/TLTextShape.ts @@ -4,7 +4,7 @@ import { TLAlignType, alignValidator } from '../styles/TLAlignStyle' import { TLColorType, colorValidator } from '../styles/TLColorStyle' import { TLFontType, fontValidator } from '../styles/TLFontStyle' import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle' -import { TLBaseShape, createShapeValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' /** @public */ export type TLTextShapeProps = { @@ -22,19 +22,16 @@ export type TLTextShapeProps = { export type TLTextShape = TLBaseShape<'text', TLTextShapeProps> /** @internal */ -export const textShapeValidator: T.Validator = createShapeValidator( - 'text', - T.object({ - color: colorValidator, - size: sizeValidator, - font: fontValidator, - align: alignValidator, - w: T.nonZeroNumber, - text: T.string, - scale: T.nonZeroNumber, - autoSize: T.boolean, - }) -) +export const textShapeProps: ShapeProps = { + color: colorValidator, + size: sizeValidator, + font: fontValidator, + align: alignValidator, + w: T.nonZeroNumber, + text: T.string, + scale: T.nonZeroNumber, + autoSize: T.boolean, +} const Versions = { RemoveJustify: 1, diff --git a/packages/tlschema/src/shapes/TLVideoShape.ts b/packages/tlschema/src/shapes/TLVideoShape.ts index 9f9545a6c..b5293d01a 100644 --- a/packages/tlschema/src/shapes/TLVideoShape.ts +++ b/packages/tlschema/src/shapes/TLVideoShape.ts @@ -2,7 +2,7 @@ import { defineMigrations } from '@tldraw/store' import { T } from '@tldraw/validate' import { assetIdValidator } from '../assets/TLBaseAsset' import { TLAssetId } from '../records/TLAsset' -import { TLBaseShape, createShapeValidator } from './TLBaseShape' +import { ShapeProps, TLBaseShape } from './TLBaseShape' /** @public */ export type TLVideoShapeProps = { @@ -18,17 +18,14 @@ export type TLVideoShapeProps = { export type TLVideoShape = TLBaseShape<'video', TLVideoShapeProps> /** @internal */ -export const videoShapeValidator: T.Validator = createShapeValidator( - 'video', - T.object({ - w: T.nonZeroNumber, - h: T.nonZeroNumber, - time: T.number, - playing: T.boolean, - url: T.string, - assetId: assetIdValidator.nullable(), - }) -) +export const videoShapeProps: ShapeProps = { + w: T.nonZeroNumber, + h: T.nonZeroNumber, + time: T.number, + playing: T.boolean, + url: T.string, + assetId: assetIdValidator.nullable(), +} const Versions = { AddUrlProp: 1, diff --git a/packages/utils/api-report.md b/packages/utils/api-report.md index e9271f22d..5f093af1a 100644 --- a/packages/utils/api-report.md +++ b/packages/utils/api-report.md @@ -65,6 +65,11 @@ export function getOwnProperty(obj: object, key: string): unknown; // @internal (undocumented) export function hasOwnProperty(obj: object, key: string): boolean; +// @internal (undocumented) +export type Identity = { + [K in keyof T]: T[K]; +}; + // @public export function isDefined(value: T): value is typeof value extends undefined ? never : T; @@ -83,12 +88,22 @@ export function lerp(a: number, b: number, t: number): number; // @public (undocumented) export function lns(str: string): string; +// @internal +export function mapObjectMapValues(object: { + readonly [K in Key]: ValueBefore; +}, mapper: (key: Key, value: ValueBefore) => ValueAfter): { + [K in Key]: ValueAfter; +}; + // @internal (undocumented) export function minBy(arr: readonly T[], fn: (item: T) => number): T | undefined; // @public export function modulate(value: number, rangeA: number[], rangeB: number[], clamp?: boolean): number; +// @internal +export function noop(): void; + // @internal export function objectMapEntries(object: { [K in Key]: Value; @@ -135,6 +150,10 @@ export type RecursivePartial = { [P in keyof T]?: RecursivePartial; }; +// @internal (undocumented) +type Required_2 = Identity & _Required>>; +export { Required_2 as Required } + // @public (undocumented) export type Result = ErrorResult | OkResult; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 2f597f90e..e9b0e7b68 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -10,7 +10,7 @@ export { } from './lib/control' export { debounce } from './lib/debounce' export { annotateError, getErrorAnnotations } from './lib/error' -export { omitFromStackTrace, throttle } from './lib/function' +export { noop, omitFromStackTrace, throttle } from './lib/function' export { getHashForObject, getHashForString, lns } from './lib/hash' export { getFirstFromIterable } from './lib/iterable' export { lerp, modulate, rng } from './lib/number' @@ -19,6 +19,7 @@ export { filterEntries, getOwnProperty, hasOwnProperty, + mapObjectMapValues, objectMapEntries, objectMapFromEntries, objectMapKeys, @@ -26,5 +27,5 @@ export { } from './lib/object' export { rafThrottle, throttledRaf } from './lib/raf' export { sortById } from './lib/sort' -export type { RecursivePartial } from './lib/types' +export type { Identity, RecursivePartial, Required } from './lib/types' export { isDefined, isNonNull, isNonNullish, structuredClone } from './lib/value' diff --git a/packages/utils/src/lib/function.ts b/packages/utils/src/lib/function.ts index d3fd248d8..0e7d1f9e8 100644 --- a/packages/utils/src/lib/function.ts +++ b/packages/utils/src/lib/function.ts @@ -52,3 +52,10 @@ export function omitFromStackTrace, Return>( return wrappedFn } + +/** + * Does nothing, but it's really really good at it. + * @internal + */ +// eslint-disable-next-line @typescript-eslint/no-empty-function +export function noop(): void {} diff --git a/packages/utils/src/lib/object.ts b/packages/utils/src/lib/object.ts index f94262658..1e5af5865 100644 --- a/packages/utils/src/lib/object.ts +++ b/packages/utils/src/lib/object.ts @@ -120,3 +120,20 @@ export function filterEntries( } return didChange ? (result as { [K in Key]: Value }) : object } + +/** + * Maps the values of one object map to another. + * @returns a new object with the entries mapped + * @internal + */ +export function mapObjectMapValues( + object: { readonly [K in Key]: ValueBefore }, + mapper: (key: Key, value: ValueBefore) => ValueAfter +): { [K in Key]: ValueAfter } { + const result = {} as { [K in Key]: ValueAfter } + for (const [key, value] of objectMapEntries(object)) { + const newValue = mapper(key, value) + result[key] = newValue + } + return result +} diff --git a/packages/utils/src/lib/types.ts b/packages/utils/src/lib/types.ts index c1ddab88d..77109009a 100644 --- a/packages/utils/src/lib/types.ts +++ b/packages/utils/src/lib/types.ts @@ -2,3 +2,11 @@ export type RecursivePartial = { [P in keyof T]?: RecursivePartial } + +/** @internal */ +export type Identity = { [K in keyof T]: T[K] } + +type _Required = { [K in keyof T]-?: T[K] } + +/** @internal */ +export type Required = Identity & _Required>> diff --git a/packages/validate/api-report.md b/packages/validate/api-report.md index d8e22970b..1a24e6a08 100644 --- a/packages/validate/api-report.md +++ b/packages/validate/api-report.md @@ -11,13 +11,13 @@ const any: Validator; const array: Validator; // @public -function arrayOf(itemValidator: Validator): ArrayOfValidator; +function arrayOf(itemValidator: Validatable): ArrayOfValidator; // @public (undocumented) class ArrayOfValidator extends Validator { - constructor(itemValidator: Validator); + constructor(itemValidator: Validatable); // (undocumented) - readonly itemValidator: Validator; + readonly itemValidator: Validatable; // (undocumented) lengthGreaterThan1(): Validator; // (undocumented) @@ -39,15 +39,15 @@ const boxModel: ObjectValidator<{ }>; // @public -function dict(keyValidator: Validator, valueValidator: Validator): DictValidator; +function dict(keyValidator: Validatable, valueValidator: Validatable): DictValidator; // @public (undocumented) class DictValidator extends Validator> { - constructor(keyValidator: Validator, valueValidator: Validator); + constructor(keyValidator: Validatable, valueValidator: Validatable); // (undocumented) - readonly keyValidator: Validator; + readonly keyValidator: Validatable; // (undocumented) - readonly valueValidator: Validator; + readonly valueValidator: Validatable; } // @public @@ -59,7 +59,7 @@ function literal(expectedValue: T): Validat // @public function model(name: string, validator: Validator): Validator; +}>(name: string, validator: Validatable): Validator; // @public const nonZeroInteger: Validator; @@ -72,22 +72,22 @@ const number: Validator; // @public function object(config: { - readonly [K in keyof Shape]: Validator; + readonly [K in keyof Shape]: Validatable; }): ObjectValidator; // @public (undocumented) class ObjectValidator extends Validator { constructor(config: { - readonly [K in keyof Shape]: Validator; + readonly [K in keyof Shape]: Validatable; }, shouldAllowUnknownProperties?: boolean); // (undocumented) allowUnknownProperties(): ObjectValidator; // (undocumented) readonly config: { - readonly [K in keyof Shape]: Validator; + readonly [K in keyof Shape]: Validatable; }; extend>(extension: { - readonly [K in keyof Extension]: Validator; + readonly [K in keyof Extension]: Validatable; }): ObjectValidator; } @@ -120,6 +120,7 @@ declare namespace T { model, setEnum, ValidatorFn, + Validatable, ValidationError, TypeOf, Validator, @@ -147,7 +148,7 @@ declare namespace T { export { T } // @public (undocumented) -type TypeOf> = V extends Validator ? T : never; +type TypeOf> = V extends Validatable ? T : never; // @public function union>(key: Key, config: Config): UnionValidator; @@ -165,6 +166,11 @@ const unknown: Validator; // @public (undocumented) const unknownObject: Validator>; +// @public (undocumented) +type Validatable = { + validate: (value: unknown) => T; +}; + // @public (undocumented) class ValidationError extends Error { constructor(rawMessage: string, path?: ReadonlyArray); @@ -177,7 +183,7 @@ class ValidationError extends Error { } // @public (undocumented) -class Validator { +class Validator implements Validatable { constructor(validationFn: ValidatorFn); check(name: string, checkFn: (value: T) => void): Validator; // (undocumented) diff --git a/packages/validate/src/lib/validation.ts b/packages/validate/src/lib/validation.ts index 048d3f8d5..1b4914064 100644 --- a/packages/validate/src/lib/validation.ts +++ b/packages/validate/src/lib/validation.ts @@ -3,6 +3,9 @@ import { exhaustiveSwitchError, getOwnProperty, hasOwnProperty } from '@tldraw/u /** @public */ export type ValidatorFn = (value: unknown) => T +/** @public */ +export type Validatable = { validate: (value: unknown) => T } + function formatPath(path: ReadonlyArray): string | null { if (!path.length) { return null @@ -77,10 +80,10 @@ function typeToString(value: unknown): string { } /** @public */ -export type TypeOf> = V extends Validator ? T : never +export type TypeOf> = V extends Validatable ? T : never /** @public */ -export class Validator { +export class Validator implements Validatable { constructor(readonly validationFn: ValidatorFn) {} /** @@ -159,7 +162,7 @@ export class Validator { /** @public */ export class ArrayOfValidator extends Validator { - constructor(readonly itemValidator: Validator) { + constructor(readonly itemValidator: Validatable) { super((value) => { const arr = array.validate(value) for (let i = 0; i < arr.length; i++) { @@ -190,7 +193,7 @@ export class ArrayOfValidator extends Validator { export class ObjectValidator extends Validator { constructor( public readonly config: { - readonly [K in keyof Shape]: Validator + readonly [K in keyof Shape]: Validatable }, private readonly shouldAllowUnknownProperties = false ) { @@ -236,7 +239,7 @@ export class ObjectValidator extends Validator { * ``` */ extend>(extension: { - readonly [K in keyof Extension]: Validator + readonly [K in keyof Extension]: Validatable }): ObjectValidator { return new ObjectValidator({ ...this.config, ...extension }) as ObjectValidator< Shape & Extension @@ -246,7 +249,7 @@ export class ObjectValidator extends Validator { // pass this into itself e.g. Config extends UnionObjectSchemaConfig type UnionValidatorConfig = { - readonly [Variant in keyof Config]: Validator & { + readonly [Variant in keyof Config]: Validatable & { validate: (input: any) => { readonly [K in Key]: Variant } } } @@ -292,8 +295,8 @@ export class UnionValidator< /** @public */ export class DictValidator extends Validator> { constructor( - public readonly keyValidator: Validator, - public readonly valueValidator: Validator + public readonly keyValidator: Validatable, + public readonly valueValidator: Validatable ) { super((object) => { if (typeof object !== 'object' || object === null) { @@ -446,7 +449,7 @@ export const array = new Validator((value) => { * * @public */ -export function arrayOf(itemValidator: Validator): ArrayOfValidator { +export function arrayOf(itemValidator: Validatable): ArrayOfValidator { return new ArrayOfValidator(itemValidator) } @@ -464,7 +467,7 @@ export const unknownObject = new Validator>((value) => { * @public */ export function object(config: { - readonly [K in keyof Shape]: Validator + readonly [K in keyof Shape]: Validatable }): ObjectValidator { return new ObjectValidator(config) } @@ -475,8 +478,8 @@ export function object(config: { * @public */ export function dict( - keyValidator: Validator, - valueValidator: Validator + keyValidator: Validatable, + valueValidator: Validatable ): DictValidator { return new DictValidator(keyValidator, valueValidator) } @@ -517,7 +520,7 @@ export function union( name: string, - validator: Validator + validator: Validatable ): Validator { return new Validator((value) => { const prefix =