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