diff --git a/apps/dotcom/src/utils/context-menu/CursorChatMenuItem.tsx b/apps/dotcom/src/utils/context-menu/CursorChatMenuItem.tsx
new file mode 100644
index 000000000..50e7603d0
--- /dev/null
+++ b/apps/dotcom/src/utils/context-menu/CursorChatMenuItem.tsx
@@ -0,0 +1,18 @@
+import { TldrawUiMenuItem, useActions, useEditor, useValue } from '@tldraw/tldraw'
+import { CURSOR_CHAT_ACTION } from '../useCursorChat'
+
+export function CursorChatMenuItem() {
+ const editor = useEditor()
+ const actions = useActions()
+ const shouldShow = useValue(
+ 'show cursor chat',
+ () => {
+ return editor.getInstanceState().isCoarsePointer && !editor.getSelectedShapes().length
+ },
+ [editor]
+ )
+
+ if (!shouldShow) return null
+
+ return
+}
diff --git a/apps/dotcom/src/utils/links.ts b/apps/dotcom/src/utils/links.ts
deleted file mode 100644
index b6e912a78..000000000
--- a/apps/dotcom/src/utils/links.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { menuGroup, menuItem, TLUiOverrides } from '@tldraw/tldraw'
-
-export const GITHUB_URL = 'https://github.com/tldraw/tldraw'
-
-const linksMenuGroup = menuGroup(
- 'links',
- menuItem({
- id: 'github',
- label: 'help-menu.github',
- readonlyOk: true,
- icon: 'github',
- onSelect() {
- window.open(GITHUB_URL)
- },
- }),
- menuItem({
- id: 'twitter',
- label: 'help-menu.twitter',
- icon: 'twitter',
- readonlyOk: true,
- onSelect() {
- window.open('https://twitter.com/tldraw')
- },
- }),
- menuItem({
- id: 'discord',
- label: 'help-menu.discord',
- icon: 'discord',
- readonlyOk: true,
- onSelect() {
- window.open('https://discord.gg/SBBEVCA4PG')
- },
- }),
- menuItem({
- id: 'about',
- label: 'help-menu.about',
- icon: 'external-link',
- readonlyOk: true,
- onSelect() {
- window.open('https://www.tldraw.dev')
- },
- })
-)!
-
-export const linksUiOverrides: TLUiOverrides = {
- helpMenu(editor, schema) {
- schema.push(linksMenuGroup)
- return schema
- },
- menu(editor, schema, { isMobile }) {
- if (isMobile) {
- schema.push(linksMenuGroup)
- }
- return schema
- },
-}
diff --git a/apps/dotcom/src/utils/migration/DebugMenuItems.tsx b/apps/dotcom/src/utils/migration/DebugMenuItems.tsx
index fbc6f7e9f..e098e9e0c 100644
--- a/apps/dotcom/src/utils/migration/DebugMenuItems.tsx
+++ b/apps/dotcom/src/utils/migration/DebugMenuItems.tsx
@@ -1,30 +1,28 @@
-import { DropdownMenu } from '@tldraw/tldraw'
+import { TldrawUiMenuGroup, TldrawUiMenuItem } from '@tldraw/tldraw'
import { env } from '../env'
const RELEASE_INFO = `${env} ${process.env.NEXT_PUBLIC_TLDRAW_RELEASE_INFO ?? 'unreleased'}`
export function DebugMenuItems() {
return (
-
- {
+
+ {
window.alert(`${RELEASE_INFO}`)
}}
- title={`${RELEASE_INFO}`}
- >
- Version
-
- {
+ />
+ {
const { writeV1ContentsToIdb } = await import('./writeV1ContentsToIdb')
await writeV1ContentsToIdb()
window.location.reload()
}}
- >
- Test v1 content
-
-
+ />
+
)
}
diff --git a/apps/dotcom/src/utils/sharing.ts b/apps/dotcom/src/utils/sharing.ts
index 6bf21ad8c..0f2db60d4 100644
--- a/apps/dotcom/src/utils/sharing.ts
+++ b/apps/dotcom/src/utils/sharing.ts
@@ -12,11 +12,7 @@ import {
TLUiOverrides,
TLUiToastsContextType,
TLUiTranslationKey,
- assert,
- findMenuItem,
isShape,
- menuGroup,
- menuItem,
} from '@tldraw/tldraw'
import { useMemo } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
@@ -30,8 +26,9 @@ import { UI_OVERRIDE_TODO_EVENT, useHandleUiEvents } from './useHandleUiEvent'
export const SHARE_PROJECT_ACTION = 'share-project' as const
export const SHARE_SNAPSHOT_ACTION = 'share-snapshot' as const
-const LEAVE_SHARED_PROJECT_ACTION = 'leave-shared-project' as const
+export const LEAVE_SHARED_PROJECT_ACTION = 'leave-shared-project' as const
export const FORK_PROJECT_ACTION = 'fork-project' as const
+
const CREATE_SNAPSHOT_ENDPOINT = `/api/snapshots`
const SNAPSHOT_UPLOAD_URL = `/api/new-room`
@@ -93,7 +90,7 @@ async function getSnapshotLink(
})
}
-export function useSharing({ isMultiplayer }: { isMultiplayer: boolean }): TLUiOverrides {
+export function useSharing(): TLUiOverrides {
const navigate = useNavigate()
const id = useSearchParams()[0].get('id') ?? undefined
const uploadFileToAsset = useMultiplayerAssets(ASSET_UPLOADER_URL)
@@ -188,24 +185,8 @@ export function useSharing({ isMultiplayer }: { isMultiplayer: boolean }): TLUiO
}
return actions
},
- menu(editor, menu, { actions }) {
- const fileMenu = findMenuItem(menu, ['menu', 'file'])
- assert(fileMenu.type === 'submenu')
- if (isMultiplayer) {
- fileMenu.children.unshift(
- menuGroup(
- 'share',
- menuItem(actions[FORK_PROJECT_ACTION]),
- menuItem(actions[LEAVE_SHARED_PROJECT_ACTION])
- )!
- )
- } else {
- fileMenu.children.unshift(menuGroup('share', menuItem(actions[SHARE_PROJECT_ACTION]))!)
- }
- return menu
- },
}),
- [handleUiEvent, navigate, uploadFileToAsset, id, isMultiplayer]
+ [handleUiEvent, navigate, uploadFileToAsset, id]
)
}
diff --git a/apps/dotcom/src/utils/shouldClearDocument.tsx b/apps/dotcom/src/utils/shouldClearDocument.tsx
index def4f44fd..2512aece6 100644
--- a/apps/dotcom/src/utils/shouldClearDocument.tsx
+++ b/apps/dotcom/src/utils/shouldClearDocument.tsx
@@ -1,4 +1,13 @@
-import { Button, Dialog, TLUiDialogsContextType, useTranslation } from '@tldraw/tldraw'
+import {
+ Button,
+ DialogBody,
+ DialogCloseButton,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ TLUiDialogsContextType,
+ useTranslation,
+} from '@tldraw/tldraw'
import { useState } from 'react'
import { userPreferences } from './userPreferences'
@@ -40,14 +49,14 @@ function ConfirmClearDialog({
const [dontShowAgain, setDontShowAgain] = useState(false)
return (
<>
-
- {msg('file-system.confirm-clear.title')}
-
-
-
+
+ {msg('file-system.confirm-clear.title')}
+
+
+
{msg('file-system.confirm-clear.description')}
-
-
+
+
-
+
>
)
}
diff --git a/apps/dotcom/src/utils/shouldLeaveSharedProject.tsx b/apps/dotcom/src/utils/shouldLeaveSharedProject.tsx
index cd9fd8df5..10a8b4e4e 100644
--- a/apps/dotcom/src/utils/shouldLeaveSharedProject.tsx
+++ b/apps/dotcom/src/utils/shouldLeaveSharedProject.tsx
@@ -1,6 +1,10 @@
import {
Button,
- Dialog,
+ DialogBody,
+ DialogCloseButton,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
TLUiDialogsContextType,
useLocalStorageState,
useTranslation,
@@ -46,14 +50,12 @@ function ConfirmLeaveDialog({
return (
<>
-
- {msg('sharing.confirm-leave.title')}
-
-
-
- {msg('sharing.confirm-leave.description')}
-
-
+
+ {msg('sharing.confirm-leave.title')}
+
+
+ {msg('sharing.confirm-leave.description')}
+
-
+
>
)
}
diff --git a/apps/dotcom/src/utils/shouldOverrideDocument.tsx b/apps/dotcom/src/utils/shouldOverrideDocument.tsx
index bbfdd5d59..bb5d0dee0 100644
--- a/apps/dotcom/src/utils/shouldOverrideDocument.tsx
+++ b/apps/dotcom/src/utils/shouldOverrideDocument.tsx
@@ -1,4 +1,13 @@
-import { Button, Dialog, TLUiDialogsContextType, useTranslation } from '@tldraw/tldraw'
+import {
+ Button,
+ DialogBody,
+ DialogCloseButton,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ TLUiDialogsContextType,
+ useTranslation,
+} from '@tldraw/tldraw'
import { useState } from 'react'
import { userPreferences } from './userPreferences'
@@ -40,14 +49,14 @@ function ConfirmOpenDialog({
const [dontShowAgain, setDontShowAgain] = useState(false)
return (
<>
-
- {msg('file-system.confirm-open.title')}
-
-
-
+
+ {msg('file-system.confirm-open.title')}
+
+
+
{msg('file-system.confirm-open.description')}
-
-
+
+
-
+
>
)
}
diff --git a/apps/dotcom/src/utils/url.ts b/apps/dotcom/src/utils/url.ts
new file mode 100644
index 000000000..839eb9e89
--- /dev/null
+++ b/apps/dotcom/src/utils/url.ts
@@ -0,0 +1,3 @@
+export function openUrl(url: string) {
+ window.open(url, '_blank')
+}
diff --git a/apps/dotcom/src/utils/useCursorChat.ts b/apps/dotcom/src/utils/useCursorChat.ts
index 75bd743bd..2d6fe6684 100644
--- a/apps/dotcom/src/utils/useCursorChat.ts
+++ b/apps/dotcom/src/utils/useCursorChat.ts
@@ -1,4 +1,4 @@
-import { TLUiOverrides, menuGroup, menuItem } from '@tldraw/tldraw'
+import { TLUiOverrides } from '@tldraw/tldraw'
import { useMemo } from 'react'
import { useHandleUiEvents } from './useHandleUiEvent'
@@ -27,36 +27,6 @@ export function useCursorChat(): TLUiOverrides {
}
return actions
},
- contextMenu(editor, contextMenu, { actions }) {
- if (editor.getSelectedShapes().length > 0 || editor.getInstanceState().isCoarsePointer) {
- return contextMenu
- }
-
- const cursorChatGroup = menuGroup('cursor-chat', menuItem(actions[CURSOR_CHAT_ACTION]))
- if (!cursorChatGroup) {
- return contextMenu
- }
-
- const clipboardGroupIndex = contextMenu.findIndex((group) => group.id === 'clipboard-group')
- if (clipboardGroupIndex === -1) {
- contextMenu.push(cursorChatGroup)
- return contextMenu
- }
-
- contextMenu.splice(clipboardGroupIndex + 1, 0, cursorChatGroup)
- return contextMenu
- },
- keyboardShortcutsMenu(editor, keyboardShortcutsMenu, { actions }) {
- const group = menuGroup(
- 'shortcuts-dialog.collaboration',
- menuItem(actions[CURSOR_CHAT_ACTION])
- )
- if (!group) {
- return keyboardShortcutsMenu
- }
- keyboardShortcutsMenu.push(group)
- return keyboardShortcutsMenu
- },
}),
[handleUiEvent]
)
diff --git a/apps/dotcom/src/utils/useFileSystem.tsx b/apps/dotcom/src/utils/useFileSystem.tsx
index 095698a0b..2329b0fb7 100644
--- a/apps/dotcom/src/utils/useFileSystem.tsx
+++ b/apps/dotcom/src/utils/useFileSystem.tsx
@@ -5,10 +5,6 @@ import {
TLUiActionItem,
TLUiEventHandler,
TLUiOverrides,
- assert,
- findMenuItem,
- menuGroup,
- menuItem,
parseAndLoadDocument,
serializeTldrawJsonBlob,
transact,
@@ -19,9 +15,9 @@ import { shouldClearDocument } from './shouldClearDocument'
import { shouldOverrideDocument } from './shouldOverrideDocument'
import { useHandleUiEvents } from './useHandleUiEvent'
-const SAVE_FILE_COPY_ACTION = 'save-file-copy'
-const OPEN_FILE_ACTION = 'open-file'
-const NEW_PROJECT_ACTION = 'new-file'
+export const SAVE_FILE_COPY_ACTION = 'save-file-copy'
+export const OPEN_FILE_ACTION = 'open-file'
+export const NEW_PROJECT_ACTION = 'new-file'
const saveFileNames = new WeakMap
()
@@ -92,31 +88,6 @@ export function useFileSystem({ isMultiplayer }: { isMultiplayer: boolean }): TL
}
return actions
},
- menu(editor, menu, { actions }) {
- const fileMenu = findMenuItem(menu, ['menu', 'file'])
- assert(fileMenu.type === 'submenu')
-
- const saveItem = menuItem(actions[SAVE_FILE_COPY_ACTION])
- const openItem = menuItem(actions[OPEN_FILE_ACTION])
- const newItem = menuItem(actions[NEW_PROJECT_ACTION])
- const group = isMultiplayer
- ? // open is not currently supported in multiplayer
- menuGroup('filesystem', saveItem)
- : menuGroup('filesystem', newItem, openItem, saveItem)
- fileMenu.children.unshift(group!)
-
- return menu
- },
- keyboardShortcutsMenu(editor, menu, { actions }) {
- const fileItems = findMenuItem(menu, ['shortcuts-dialog.file'])
- assert(fileItems.type === 'group')
- fileItems.children.unshift(menuItem(actions[SAVE_FILE_COPY_ACTION]))
- if (!isMultiplayer) {
- fileItems.children.unshift(menuItem(actions[OPEN_FILE_ACTION]))
- }
-
- return menu
- },
}
}, [isMultiplayer, handleUiEvent])
}
diff --git a/apps/examples/e2e/tests/context-menu.spec.ts b/apps/examples/e2e/tests/context-menu.spec.ts
new file mode 100644
index 000000000..0741f33d9
--- /dev/null
+++ b/apps/examples/e2e/tests/context-menu.spec.ts
@@ -0,0 +1,42 @@
+import test, { Page, expect } from '@playwright/test'
+import { setupPage, setupPageWithShapes } from '../shared-e2e'
+
+declare const __tldraw_ui_event: { name: string }
+
+// We're just testing the events, not the actual results.
+
+let page: Page
+
+test.describe('Context menu', async () => {
+ test.beforeEach(async ({ browser }) => {
+ page = await browser.newPage()
+ await setupPage(page)
+ await setupPageWithShapes(page)
+ })
+
+ test('distribute horizontal', async () => {
+ // distribute horizontal
+ await page.keyboard.press('Control+a')
+ await page.mouse.click(200, 200, { button: 'right' })
+ await page.getByTestId('context-menu-sub-trigger.arrange').click()
+ await page.getByTestId('context-menu.distribute-horizontal').focus()
+ await page.keyboard.press('Enter')
+ expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
+ name: 'distribute-shapes',
+ data: { operation: 'horizontal', source: 'context-menu' },
+ })
+ })
+
+ test('distribute vertical', async () => {
+ // distribute vertical — Shift+Alt+V
+ await page.keyboard.press('Control+a')
+ await page.mouse.click(200, 200, { button: 'right' })
+ await page.getByTestId('context-menu-sub-trigger.arrange').click()
+ await page.getByTestId('context-menu.distribute-vertical').focus()
+ await page.keyboard.press('Enter')
+ expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
+ name: 'distribute-shapes',
+ data: { operation: 'vertical', source: 'context-menu' },
+ })
+ })
+})
diff --git a/apps/examples/e2e/tests/export-snapshots.spec.ts b/apps/examples/e2e/tests/export-snapshots.spec.ts
index e3a3e9e55..9de66df78 100644
--- a/apps/examples/e2e/tests/export-snapshots.spec.ts
+++ b/apps/examples/e2e/tests/export-snapshots.spec.ts
@@ -1,10 +1,13 @@
-import test, { Page, expect } from '@playwright/test'
-import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
-import assert from 'assert'
-import { rename, writeFile } from 'fs/promises'
-import { setupPage } from '../shared-e2e'
+import test from '@playwright/test'
+import { TLShapeId, TLShapePartial } from '@tldraw/tldraw'
-declare const editor: Editor
+// import test, { Page, expect } from '@playwright/test'
+// import assert from 'assert'
+// import { rename, writeFile } from 'fs/promises'
+// import { setupPage } from '../shared-e2e'
+// import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
+
+// declare const editor: Editor
test.describe('Export snapshots', () => {
const snapshots = {
@@ -186,50 +189,50 @@ test.describe('Export snapshots', () => {
]
}
- const snapshotsToTest = Object.entries(snapshots)
- const filteredSnapshots = snapshotsToTest // maybe we filter these down, there are a lot of them
+ // const snapshotsToTest = Object.entries(snapshots)
+ // const filteredSnapshots = snapshotsToTest // maybe we filter these down, there are a lot of them
- for (const [name, shapes] of filteredSnapshots) {
- test(`Exports with ${name} in dark mode`, async ({ browser }) => {
- const page = await browser.newPage()
- await setupPage(page)
- await page.evaluate((shapes) => {
- editor.user.updateUserPreferences({ isDarkMode: true })
- editor
- .updateInstanceState({ exportBackground: false })
- .selectAll()
- .deleteShapes(editor.getSelectedShapeIds())
- .createShapes(shapes)
- }, shapes as any)
+ // for (const [name, shapes] of filteredSnapshots) {
+ // test(`Exports with ${name} in dark mode`, async ({ browser }) => {
+ // const page = await browser.newPage()
+ // await setupPage(page)
+ // await page.evaluate((shapes) => {
+ // editor.user.updateUserPreferences({ isDarkMode: true })
+ // editor
+ // .updateInstanceState({ exportBackground: false })
+ // .selectAll()
+ // .deleteShapes(editor.getSelectedShapeIds())
+ // .createShapes(shapes)
+ // }, shapes as any)
- await snapshotTest(page)
- })
- }
+ // await snapshotTest(page)
+ // })
+ // }
- async function snapshotTest(page: Page) {
- const downloadAndSnapshot = page.waitForEvent('download').then(async (download) => {
- const path = (await download.path()) as string
- assert(path)
- await rename(path, path + '.svg')
- await writeFile(
- path + '.html',
- `
-
-
-
-
- `,
- 'utf-8'
- )
+ // async function snapshotTest(page: Page) {
+ // const downloadAndSnapshot = page.waitForEvent('download').then(async (download) => {
+ // const path = (await download.path()) as string
+ // assert(path)
+ // await rename(path, path + '.svg')
+ // await writeFile(
+ // path + '.html',
+ // `
+ //
+ //
+ //
+ //
+ // `,
+ // 'utf-8'
+ // )
- await page.goto(`file://${path}.html`)
- const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
- await expect(page).toHaveScreenshot({
- omitBackground: true,
- clip,
- })
- })
- await page.evaluate(() => (window as any)['tldraw-export']())
- await downloadAndSnapshot
- }
+ // await page.goto(`file://${path}.html`)
+ // const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
+ // await expect(page).toHaveScreenshot({
+ // omitBackground: true,
+ // clip,
+ // })
+ // })
+ // await page.evaluate(() => (window as any)['tldraw-export']())
+ // await downloadAndSnapshot
+ // }
})
diff --git a/apps/examples/e2e/tests/test-clipboard.spec.ts b/apps/examples/e2e/tests/test-clipboard.spec.ts
index 89c1b6f41..cf5df5079 100644
--- a/apps/examples/e2e/tests/test-clipboard.spec.ts
+++ b/apps/examples/e2e/tests/test-clipboard.spec.ts
@@ -46,12 +46,12 @@ test.describe.skip('clipboard tests', () => {
expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
await page.getByTestId('main.menu').click()
- await page.getByTestId('menu-item.edit').click()
- await page.getByTestId('menu-item.copy').click()
+ await page.getByTestId('main-menu-sub-trigger.edit').click()
+ await page.getByTestId('main-menu.copy').click()
await sleep(100)
await page.getByTestId('main.menu').click()
- await page.getByTestId('menu-item.edit').click()
- await page.getByTestId('menu-item.paste').click()
+ await page.getByTestId('main-menu-sub-trigger.edit').click()
+ await page.getByTestId('main-menu.paste').click()
expect(await page.evaluate(() => editor.getCurrentPageShapes().length)).toBe(2)
expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
@@ -67,11 +67,11 @@ test.describe.skip('clipboard tests', () => {
expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
await page.mouse.click(100, 100, { button: 'right' })
- await page.getByTestId('menu-item.copy').click()
+ await page.getByTestId('main-menu.copy').click()
await sleep(100)
await page.mouse.move(200, 200)
await page.mouse.click(100, 100, { button: 'right' })
- await page.getByTestId('menu-item.paste').click()
+ await page.getByTestId('main-menu.paste').click()
expect(await page.evaluate(() => editor.getCurrentPageShapes().length)).toBe(2)
expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
diff --git a/apps/examples/e2e/tests/test-kbds.spec.ts b/apps/examples/e2e/tests/test-kbds.spec.ts
index 91986e4d0..70a9e9276 100644
--- a/apps/examples/e2e/tests/test-kbds.spec.ts
+++ b/apps/examples/e2e/tests/test-kbds.spec.ts
@@ -364,38 +364,6 @@ test.describe('Actions on shapes', () => {
})
})
-test.describe('Context menu', async () => {
- test.beforeEach(async ({ browser }) => {
- page = await browser.newPage()
- await setupPage(page)
- await setupPageWithShapes(page)
- })
-
- test('distribute horizontal', async () => {
- // distribute horizontal
- await page.keyboard.press('Control+a')
- await page.mouse.click(200, 200, { button: 'right' })
- await page.getByTestId('menu-item.arrange').click()
- await page.getByTestId('menu-item.distribute-horizontal').click()
- expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
- name: 'distribute-shapes',
- data: { operation: 'horizontal', source: 'context-menu' },
- })
- })
-
- test('distribute vertical', async () => {
- // distribute vertical — Shift+Alt+V
- await page.keyboard.press('Control+a')
- await page.mouse.click(200, 200, { button: 'right' })
- await page.getByTestId('menu-item.arrange').click()
- await page.getByTestId('menu-item.distribute-vertical').click()
- expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
- name: 'distribute-shapes',
- data: { operation: 'vertical', source: 'context-menu' },
- })
- })
-})
-
test.describe('Delete bug', () => {
test.beforeEach(async ({ browser }) => {
page = await browser.newPage()
diff --git a/apps/examples/e2e/tests/test-smoke.spec.ts b/apps/examples/e2e/tests/test-smoke.spec.ts
index 2504edcca..c6d856bea 100644
--- a/apps/examples/e2e/tests/test-smoke.spec.ts
+++ b/apps/examples/e2e/tests/test-smoke.spec.ts
@@ -26,8 +26,8 @@ test.describe('smoke tests', () => {
test('undo and redo', async ({ page }) => {
// buttons should be disabled when there is no history
- expect(page.getByTestId('main.undo')).toBeDisabled()
- expect(page.getByTestId('main.redo')).toBeDisabled()
+ expect(page.getByTestId('quick-actions.undo')).toBeDisabled()
+ expect(page.getByTestId('quick-actions.redo')).toBeDisabled()
// create a shape
await page.keyboard.press('r')
@@ -39,22 +39,22 @@ test.describe('smoke tests', () => {
expect(await getAllShapeTypes(page)).toEqual(['geo'])
// We should have an undoable shape
- expect(page.getByTestId('main.undo')).not.toBeDisabled()
- expect(page.getByTestId('main.redo')).toBeDisabled()
+ expect(page.getByTestId('quick-actions.undo')).not.toBeDisabled()
+ expect(page.getByTestId('quick-actions.redo')).toBeDisabled()
// Click the undo button to undo the shape
- await page.getByTestId('main.undo').click()
+ await page.getByTestId('quick-actions.undo').click()
expect(await getAllShapeTypes(page)).toEqual([])
- expect(page.getByTestId('main.undo')).toBeDisabled()
- expect(page.getByTestId('main.redo')).not.toBeDisabled()
+ expect(page.getByTestId('quick-actions.undo')).toBeDisabled()
+ expect(page.getByTestId('quick-actions.redo')).not.toBeDisabled()
// Click the redo button to redo the shape
- await page.getByTestId('main.redo').click()
+ await page.getByTestId('quick-actions.redo').click()
expect(await getAllShapeTypes(page)).toEqual(['geo'])
- expect(await page.getByTestId('main.undo').isDisabled()).not.toBe(true)
- expect(await page.getByTestId('main.redo').isDisabled()).toBe(true)
+ expect(await page.getByTestId('quick-actions.undo').isDisabled()).not.toBe(true)
+ expect(await page.getByTestId('quick-actions.redo').isDisabled()).toBe(true)
})
test('style panel + undo and redo squashing', async ({ page }) => {
@@ -108,8 +108,8 @@ test.describe('smoke tests', () => {
await page.mouse.up()
// Now undo and redo
- const undo = page.getByTestId('main.undo')
- const redo = page.getByTestId('main.redo')
+ const undo = page.getByTestId('quick-actions.undo')
+ const redo = page.getByTestId('quick-actions.redo')
await undo.click() // orange -> light blue
expect(await getSelectedShapeColor()).toBe('light-blue') // skipping squashed colors!
@@ -124,7 +124,7 @@ test.describe('smoke tests', () => {
await redo.click() // black -> light blue
await redo.click() // light-blue -> orange
- expect(await page.getByTestId('main.undo').isDisabled()).not.toBe(true)
- expect(await page.getByTestId('main.redo').isDisabled()).toBe(true)
+ expect(await page.getByTestId('quick-actions.undo').isDisabled()).not.toBe(true)
+ expect(await page.getByTestId('quick-actions.redo').isDisabled()).toBe(true)
})
})
diff --git a/apps/examples/src/examples/custom-actions-menu/CustomActionsMenuExample.tsx b/apps/examples/src/examples/custom-actions-menu/CustomActionsMenuExample.tsx
new file mode 100644
index 000000000..6b42d12c6
--- /dev/null
+++ b/apps/examples/src/examples/custom-actions-menu/CustomActionsMenuExample.tsx
@@ -0,0 +1,22 @@
+import { DefaultActionsMenu, TLComponents, Tldraw } from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomActionsMenu() {
+ return (
+
+
+
+ )
+}
+
+const components: TLComponents = {
+ ActionsMenu: CustomActionsMenu,
+}
+
+export default function CustomActionsMenuExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-actions-menu/README.md b/apps/examples/src/examples/custom-actions-menu/README.md
new file mode 100644
index 000000000..00ae48688
--- /dev/null
+++ b/apps/examples/src/examples/custom-actions-menu/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom actions menu
+component: ./CustomActionsMenuExample.tsx
+category: ui
+---
+
+You can customize tldraw's actions menu.
+
+---
+
+The actions menu can be customized by providing a `ActionsMenu` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden.
diff --git a/apps/examples/src/examples/custom-config/ui-overrides.ts b/apps/examples/src/examples/custom-config/ui-overrides.tsx
similarity index 67%
rename from apps/examples/src/examples/custom-config/ui-overrides.ts
rename to apps/examples/src/examples/custom-config/ui-overrides.tsx
index 5e346d973..497f45bdc 100644
--- a/apps/examples/src/examples/custom-config/ui-overrides.ts
+++ b/apps/examples/src/examples/custom-config/ui-overrides.tsx
@@ -1,4 +1,12 @@
-import { TLUiMenuGroup, TLUiOverrides, menuItem, toolbarItem } from '@tldraw/tldraw'
+import {
+ DefaultKeyboardShortcutsDialog,
+ DefaultKeyboardShortcutsDialogContent,
+ TLComponents,
+ TLUiOverrides,
+ TldrawUiMenuItem,
+ toolbarItem,
+ useTools,
+} from '@tldraw/tldraw'
// There's a guide at the bottom of this file!
@@ -10,7 +18,6 @@ export const uiOverrides: TLUiOverrides = {
icon: 'color',
label: 'Card',
kbd: 'c',
- readonlyOk: false,
onSelect: () => {
editor.setCurrentTool('card')
},
@@ -22,13 +29,18 @@ export const uiOverrides: TLUiOverrides = {
toolbar.splice(4, 0, toolbarItem(tools.card))
return toolbar
},
- keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
- // Add the tool item from the context to the keyboard shortcuts dialog.
- const toolsGroup = keyboardShortcutsMenu.find(
- (group) => group.id === 'shortcuts-dialog.tools'
- ) as TLUiMenuGroup
- toolsGroup.children.push(menuItem(tools.card))
- return keyboardShortcutsMenu
+}
+
+export const components: TLComponents = {
+ KeyboardShortcutsDialog: (props) => {
+ const tools = useTools()
+ return (
+
+
+ {/* Ideally, we'd interleave this into the tools group */}
+
+
+ )
},
}
diff --git a/apps/examples/src/examples/custom-context-menu/CustomContextMenuExample.tsx b/apps/examples/src/examples/custom-context-menu/CustomContextMenuExample.tsx
new file mode 100644
index 000000000..683608bcd
--- /dev/null
+++ b/apps/examples/src/examples/custom-context-menu/CustomContextMenuExample.tsx
@@ -0,0 +1,41 @@
+import {
+ DefaultContextMenu,
+ DefaultContextMenuContent,
+ TLComponents,
+ TLUiContextMenuProps,
+ Tldraw,
+ TldrawUiMenuGroup,
+ TldrawUiMenuItem,
+} from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomContextMenu(props: TLUiContextMenuProps) {
+ return (
+
+
+ {
+ window.open('https://x.com/tldraw', '_blank')
+ }}
+ />
+
+
+
+ )
+}
+
+const components: TLComponents = {
+ ContextMenu: CustomContextMenu,
+}
+
+export default function CustomContextMenuExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-context-menu/README.md b/apps/examples/src/examples/custom-context-menu/README.md
new file mode 100644
index 000000000..7bc741e09
--- /dev/null
+++ b/apps/examples/src/examples/custom-context-menu/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom context menu
+component: ./CustomContextMenuExample.tsx
+category: ui
+---
+
+You can customize tldraw's context menu.
+
+---
+
+The context menu can be customized by providing a `ContextMenu` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden instead.
diff --git a/apps/examples/src/examples/custom-debug-menu/CustomDebugMenuExample.tsx b/apps/examples/src/examples/custom-debug-menu/CustomDebugMenuExample.tsx
new file mode 100644
index 000000000..77e34633e
--- /dev/null
+++ b/apps/examples/src/examples/custom-debug-menu/CustomDebugMenuExample.tsx
@@ -0,0 +1,40 @@
+import {
+ DefaultDebugMenu,
+ DefaultDebugMenuContent,
+ TLComponents,
+ Tldraw,
+ TldrawUiMenuGroup,
+ TldrawUiMenuItem,
+} from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomDebugMenu() {
+ return (
+
+
+
+ {
+ window.open('https://x.com/tldraw', '_blank')
+ }}
+ />
+
+
+ )
+}
+
+const components: TLComponents = {
+ DebugMenu: CustomDebugMenu,
+}
+
+export default function CustomDebugMenuExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-debug-menu/README.md b/apps/examples/src/examples/custom-debug-menu/README.md
new file mode 100644
index 000000000..ec2af79a6
--- /dev/null
+++ b/apps/examples/src/examples/custom-debug-menu/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom debug menu
+component: ./CustomDebugMenuExample.tsx
+category: ui
+---
+
+You can customize tldraw's debug menu.
+
+---
+
+The help menu can be customized by providing a `DebugMenu` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden.
diff --git a/apps/examples/src/examples/custom-help-menu/CustomHelpMenuExample.tsx b/apps/examples/src/examples/custom-help-menu/CustomHelpMenuExample.tsx
new file mode 100644
index 000000000..87b9583c2
--- /dev/null
+++ b/apps/examples/src/examples/custom-help-menu/CustomHelpMenuExample.tsx
@@ -0,0 +1,40 @@
+import {
+ DefaultHelpMenu,
+ DefaultHelpMenuContent,
+ TLComponents,
+ Tldraw,
+ TldrawUiMenuGroup,
+ TldrawUiMenuItem,
+} from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomHelpMenu() {
+ return (
+
+
+ {
+ window.open('https://x.com/tldraw', '_blank')
+ }}
+ />
+
+
+
+ )
+}
+
+const components: TLComponents = {
+ HelpMenu: CustomHelpMenu,
+}
+
+export default function CustomHelpMenuExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-help-menu/README.md b/apps/examples/src/examples/custom-help-menu/README.md
new file mode 100644
index 000000000..9bfbeed96
--- /dev/null
+++ b/apps/examples/src/examples/custom-help-menu/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom help menu
+component: ./CustomHelpMenuExample.tsx
+category: ui
+---
+
+You can customize tldraw's help menu.
+
+---
+
+The help menu can be customized by providing a `HelpMenu` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden.
diff --git a/apps/examples/src/examples/custom-keyboard-shortcuts-dialog/CustomKeyboardShortcutsDialogExample.tsx b/apps/examples/src/examples/custom-keyboard-shortcuts-dialog/CustomKeyboardShortcutsDialogExample.tsx
new file mode 100644
index 000000000..4a9cd99ba
--- /dev/null
+++ b/apps/examples/src/examples/custom-keyboard-shortcuts-dialog/CustomKeyboardShortcutsDialogExample.tsx
@@ -0,0 +1,38 @@
+import {
+ DefaultKeyboardShortcutsDialog,
+ DefaultKeyboardShortcutsDialogContent,
+ TLComponents,
+ TLUiKeyboardShortcutsDialogProps,
+ Tldraw,
+ TldrawUiMenuItem,
+} from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomKeyboardShortcutsDialog(props: TLUiKeyboardShortcutsDialogProps) {
+ return (
+
+ {
+ window.open('https://x.com/tldraw', '_blank')
+ }}
+ />
+
+
+ )
+}
+
+const components: TLComponents = {
+ KeyboardShortcutsDialog: CustomKeyboardShortcutsDialog,
+}
+
+export default function CustomKeyboardShortcutsDialogExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-keyboard-shortcuts-dialog/README.md b/apps/examples/src/examples/custom-keyboard-shortcuts-dialog/README.md
new file mode 100644
index 000000000..3bd55a3ce
--- /dev/null
+++ b/apps/examples/src/examples/custom-keyboard-shortcuts-dialog/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom keyboard shortcuts dialog
+component: ./CustomKeyboardShortcutsDialogExample.tsx
+category: ui
+---
+
+You can customize tldraw's keyboard shortcuts dialog.
+
+---
+
+The keyboard shortcuts dialog can be customized by providing a `KeyboardShortcutsDialog` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden instead.
diff --git a/apps/examples/src/examples/custom-main-menu/CustomMainMenuExample.tsx b/apps/examples/src/examples/custom-main-menu/CustomMainMenuExample.tsx
new file mode 100644
index 000000000..0dcb99a93
--- /dev/null
+++ b/apps/examples/src/examples/custom-main-menu/CustomMainMenuExample.tsx
@@ -0,0 +1,40 @@
+import {
+ DefaultMainMenu,
+ DefaultMainMenuContent,
+ TLComponents,
+ Tldraw,
+ TldrawUiMenuGroup,
+ TldrawUiMenuItem,
+} from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomMainMenu() {
+ return (
+
+
+ {
+ window.open('https://x.com/tldraw', '_blank')
+ }}
+ />
+
+
+
+ )
+}
+
+const components: TLComponents = {
+ MainMenu: CustomMainMenu,
+}
+
+export default function CustomMainMenuExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-main-menu/README.md b/apps/examples/src/examples/custom-main-menu/README.md
new file mode 100644
index 000000000..d44c20839
--- /dev/null
+++ b/apps/examples/src/examples/custom-main-menu/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom main menu
+component: ./CustomMainMenuExample.tsx
+category: ui
+---
+
+You can customize tldraw's main menu.
+
+---
+
+The actions menu can be customized by providing a `MainMenu` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden.
diff --git a/apps/examples/src/examples/custom-navigation-panel/CustomNavigationPanelExample.tsx b/apps/examples/src/examples/custom-navigation-panel/CustomNavigationPanelExample.tsx
new file mode 100644
index 000000000..ad530fc56
--- /dev/null
+++ b/apps/examples/src/examples/custom-navigation-panel/CustomNavigationPanelExample.tsx
@@ -0,0 +1,18 @@
+import { TLComponents, Tldraw } from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomNavigationPanel() {
+ return here you are
+}
+
+const components: TLComponents = {
+ NavigationPanel: CustomNavigationPanel, // null will hide the panel instead
+}
+
+export default function CustomNagiationPanelExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-navigation-panel/README.md b/apps/examples/src/examples/custom-navigation-panel/README.md
new file mode 100644
index 000000000..5be8bf72f
--- /dev/null
+++ b/apps/examples/src/examples/custom-navigation-panel/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom navigation panel
+component: ./CustomNavigationPanelExample.tsx
+category: ui
+---
+
+You can customize tldraw's navigation panel or remove it entirely.
+
+---
+
+The navigation panel can be customized by providing a `NavigationPanel` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden instead.
diff --git a/apps/examples/src/examples/custom-page-menu/CustomPageMenuExample.tsx b/apps/examples/src/examples/custom-page-menu/CustomPageMenuExample.tsx
new file mode 100644
index 000000000..603cdd352
--- /dev/null
+++ b/apps/examples/src/examples/custom-page-menu/CustomPageMenuExample.tsx
@@ -0,0 +1,22 @@
+import { DefaultPageMenu, TLComponents, Tldraw } from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomPageMenu() {
+ return (
+
+
+
+ )
+}
+
+const components: TLComponents = {
+ PageMenu: CustomPageMenu, // null will hide the page menu instead
+}
+
+export default function CustomPageMenuExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-page-menu/README.md b/apps/examples/src/examples/custom-page-menu/README.md
new file mode 100644
index 000000000..38b1fcd49
--- /dev/null
+++ b/apps/examples/src/examples/custom-page-menu/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom page menu
+component: ./CustomPageMenuExample.tsx
+category: ui
+---
+
+You can customize tldraw's page menu, or remove it entirely.
+
+---
+
+The page menu can be customized by providing a `PageMenu` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden.
diff --git a/apps/examples/src/examples/custom-quick-actions/CustomQuickActions.tsx b/apps/examples/src/examples/custom-quick-actions/CustomQuickActions.tsx
new file mode 100644
index 000000000..9924b7023
--- /dev/null
+++ b/apps/examples/src/examples/custom-quick-actions/CustomQuickActions.tsx
@@ -0,0 +1,29 @@
+import {
+ Button,
+ DefaultQuickActions,
+ DefaultQuickActionsContent,
+ TLComponents,
+ Tldraw,
+} from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomQuickActions() {
+ return (
+
+
+
+
+ )
+}
+
+const components: TLComponents = {
+ QuickActions: CustomQuickActions, // null will hide the page menu instead
+}
+
+export default function CustomQuickActionsExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-quick-actions/README.md b/apps/examples/src/examples/custom-quick-actions/README.md
new file mode 100644
index 000000000..c35a89816
--- /dev/null
+++ b/apps/examples/src/examples/custom-quick-actions/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom quick actions
+component: ./CustomQuickActions.tsx
+category: ui
+---
+
+You can customize tldraw's quick actions, a collection of components that appear next to the menu button, or in the toolbar on smaller sizes.
+
+---
+
+The quick actions component can be customized by providing a `QuickActionsContent` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden.
diff --git a/apps/examples/src/examples/custom-style-panel/CustomStylePanelExample.tsx b/apps/examples/src/examples/custom-style-panel/CustomStylePanelExample.tsx
new file mode 100644
index 000000000..76cc4eade
--- /dev/null
+++ b/apps/examples/src/examples/custom-style-panel/CustomStylePanelExample.tsx
@@ -0,0 +1,51 @@
+import {
+ Button,
+ DefaultColorStyle,
+ DefaultStylePanel,
+ DefaultStylePanelContent,
+ TLComponents,
+ TLUiStylePanelProps,
+ Tldraw,
+ useEditor,
+} from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomStylePanel(props: TLUiStylePanelProps) {
+ const editor = useEditor()
+
+ // Styles are complex, sorry. Check our DefaultStylePanel for an example.
+
+ return (
+
+
+
+
+
+ )
+}
+
+const components: TLComponents = {
+ StylePanel: CustomStylePanel, // null will hide the panel instead
+}
+
+export default function CustomStylePanelExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-style-panel/README.md b/apps/examples/src/examples/custom-style-panel/README.md
new file mode 100644
index 000000000..0843479a9
--- /dev/null
+++ b/apps/examples/src/examples/custom-style-panel/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom style panel
+component: ./CustomStylePanelExample.tsx
+category: ui
+---
+
+You can customize tldraw's style panel or remove it entirely.
+
+---
+
+The style panel can be customized by providing a `StylePanel` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden instead.
diff --git a/apps/examples/src/examples/custom-styles/CustomStylesExample.tsx b/apps/examples/src/examples/custom-styles/CustomStylesExample.tsx
index 99135e264..50dd85c9c 100644
--- a/apps/examples/src/examples/custom-styles/CustomStylesExample.tsx
+++ b/apps/examples/src/examples/custom-styles/CustomStylesExample.tsx
@@ -2,7 +2,7 @@ import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { CardShapeTool, CardShapeUtil } from './CardShape'
import { FilterStyleUi } from './FilterStyleUi'
-import { uiOverrides } from './ui-overrides'
+import { components, uiOverrides } from './ui-overrides'
// There's a guide at the bottom of this file!
@@ -19,6 +19,7 @@ export default function CustomStylesExample() {
shapeUtils={customShapeUtils}
tools={customTools}
overrides={uiOverrides}
+ components={components}
>
diff --git a/apps/examples/src/examples/custom-styles/ui-overrides.ts b/apps/examples/src/examples/custom-styles/ui-overrides.tsx
similarity index 66%
rename from apps/examples/src/examples/custom-styles/ui-overrides.ts
rename to apps/examples/src/examples/custom-styles/ui-overrides.tsx
index fd6acf743..650a97629 100644
--- a/apps/examples/src/examples/custom-styles/ui-overrides.ts
+++ b/apps/examples/src/examples/custom-styles/ui-overrides.tsx
@@ -1,4 +1,12 @@
-import { TLUiMenuGroup, TLUiOverrides, menuItem, toolbarItem } from '@tldraw/tldraw'
+import {
+ DefaultKeyboardShortcutsDialog,
+ DefaultKeyboardShortcutsDialogContent,
+ TLComponents,
+ TLUiOverrides,
+ TldrawUiMenuItem,
+ toolbarItem,
+ useTools,
+} from '@tldraw/tldraw'
// There's a guide at the bottom of this file!
@@ -9,7 +17,6 @@ export const uiOverrides: TLUiOverrides = {
icon: 'color',
label: 'Card' as any,
kbd: 'c',
- readonlyOk: false,
onSelect: () => {
editor.setCurrentTool('card')
},
@@ -20,12 +27,19 @@ export const uiOverrides: TLUiOverrides = {
toolbar.splice(4, 0, toolbarItem(tools.card))
return toolbar
},
- keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
- const toolsGroup = keyboardShortcutsMenu.find(
- (group) => group.id === 'shortcuts-dialog.tools'
- ) as TLUiMenuGroup
- toolsGroup.children.push(menuItem(tools.card))
- return keyboardShortcutsMenu
+}
+
+export const components: TLComponents = {
+ KeyboardShortcutsDialog: (props) => {
+ const tools = useTools()
+
+ return (
+
+
+ {/* Ideally, we'd interleave this into the tools section */}
+
+
+ )
},
}
diff --git a/apps/examples/src/examples/custom-toolbar/CustomToolbarExample.tsx b/apps/examples/src/examples/custom-toolbar/CustomToolbarExample.tsx
new file mode 100644
index 000000000..8f33e0f13
--- /dev/null
+++ b/apps/examples/src/examples/custom-toolbar/CustomToolbarExample.tsx
@@ -0,0 +1,23 @@
+import { TLComponents, Tldraw } from '@tldraw/tldraw'
+import { DefaultToolbar } from '@tldraw/tldraw/src/lib/ui/components/Toolbar/DefaultToolbar'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomToolbar() {
+ return (
+
+
+
+ )
+}
+
+const components: TLComponents = {
+ Toolbar: CustomToolbar, // null will hide the panel instead
+}
+
+export default function CustomToolbarExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-toolbar/README.md b/apps/examples/src/examples/custom-toolbar/README.md
new file mode 100644
index 000000000..cd6c74a2d
--- /dev/null
+++ b/apps/examples/src/examples/custom-toolbar/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom toolbar
+component: ./CustomToolbarExample.tsx
+category: ui
+---
+
+You can customize tldraw's toolbar or remove it entirely.
+
+---
+
+The toolbar can be customized by providing a `Toolbar` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden instead.
diff --git a/apps/examples/src/examples/custom-zoom-menu/CustomZoomMenuExample.tsx b/apps/examples/src/examples/custom-zoom-menu/CustomZoomMenuExample.tsx
new file mode 100644
index 000000000..2ada070e6
--- /dev/null
+++ b/apps/examples/src/examples/custom-zoom-menu/CustomZoomMenuExample.tsx
@@ -0,0 +1,40 @@
+import {
+ DefaultZoomMenu,
+ DefaultZoomMenuContent,
+ TLComponents,
+ Tldraw,
+ TldrawUiMenuGroup,
+ TldrawUiMenuItem,
+} from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+function CustomZoomMenu() {
+ return (
+
+
+ {
+ window.open('https://x.com/tldraw', '_blank')
+ }}
+ />
+
+
+
+ )
+}
+
+const components: TLComponents = {
+ ZoomMenu: CustomZoomMenu,
+}
+
+export default function CustomZoomMenuExample() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/examples/src/examples/custom-zoom-menu/README.md b/apps/examples/src/examples/custom-zoom-menu/README.md
new file mode 100644
index 000000000..6e64a5a0c
--- /dev/null
+++ b/apps/examples/src/examples/custom-zoom-menu/README.md
@@ -0,0 +1,11 @@
+---
+title: Custom zoom menu
+component: ./CustomZoomMenuExample.tsx
+category: ui
+---
+
+You can customize tldraw's zoom menu.
+
+---
+
+The zoom menu can be customized by providing a `ZoomMenu` component to the `Tldraw` component's `uiComponents` prop. If you provide `null`, then that component will be hidden instead.
diff --git a/apps/examples/src/examples/exploded/ExplodedExample.tsx b/apps/examples/src/examples/exploded/ExplodedExample.tsx
index 09c6e3d87..e2359f3d1 100644
--- a/apps/examples/src/examples/exploded/ExplodedExample.tsx
+++ b/apps/examples/src/examples/exploded/ExplodedExample.tsx
@@ -1,6 +1,7 @@
import {
Canvas,
ContextMenu,
+ DefaultContextMenuContent,
TldrawEditor,
TldrawHandles,
TldrawHoveredShapeIndicator,
@@ -38,8 +39,8 @@ export default function ExplodedExample() {
persistenceKey="exploded-example"
>
-
-
+ }>
+
diff --git a/apps/examples/src/examples/keyboard-shortcuts/KeyboardShortcuts.tsx b/apps/examples/src/examples/keyboard-shortcuts/KeyboardShortcuts.tsx
index 2e245e291..f8489e242 100644
--- a/apps/examples/src/examples/keyboard-shortcuts/KeyboardShortcuts.tsx
+++ b/apps/examples/src/examples/keyboard-shortcuts/KeyboardShortcuts.tsx
@@ -1,11 +1,4 @@
-import {
- TLUiActionsContextType,
- TLUiMenuGroup,
- TLUiOverrides,
- TLUiToolsContextType,
- Tldraw,
- menuItem,
-} from '@tldraw/tldraw'
+import { TLUiActionsContextType, TLUiOverrides, TLUiToolsContextType, Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import jsonSnapshot from './snapshot.json'
@@ -15,7 +8,6 @@ import jsonSnapshot from './snapshot.json'
const overrides: TLUiOverrides = {
//[a]
actions(_editor, actions): TLUiActionsContextType {
- actions['copy-as-png'].kbd = '$1'
actions['toggle-grid'].kbd = 'x'
return actions
},
@@ -24,15 +16,6 @@ const overrides: TLUiOverrides = {
tools['draw'].kbd = 'p'
return tools
},
- //[c]
- keyboardShortcutsMenu(_editor, shortcutsMenu, { actions }) {
- const editGroup = shortcutsMenu.find(
- (group) => group.id === 'shortcuts-dialog.edit'
- ) as TLUiMenuGroup
-
- editGroup.children.push(menuItem(actions['copy-as-png']))
- return shortcutsMenu
- },
}
// [2]
@@ -75,15 +58,6 @@ add a new shortcut to the keyboard shortcuts dialog [c].
We're overriding the draw tool's shortcut to 'p', maybe we want to rename it to the pen
tool or something.
-[c] keyboardShortcutsMenu
- This function takes 3 arguments, the editor instance (which we don't need), the menu
- schema, and the ui context. The shortcutsMenu is an array, so we'll need to use the
- find method to return the edit group and add our new menu item to it. Check out the
- useKeyboardShortcutsSchema.tsx file in the tldraw repo to see the full list of groups
- and the menu items they contain. menuItem() is a helper function that creates a new menu
- item for us, we just need to pass it an action or tool. We'll use the copy-as-png action
- that we modified in [a], we can grab it from the ui context's actions object.
-
[2]
Finally, we pass our overrides object into the Tldraw component's overrides prop. Now when
the component mounts, our overrides will be applied. If you open the keyboard shortcuts
diff --git a/apps/examples/src/examples/only-editor/OnlyEditor.tsx b/apps/examples/src/examples/only-editor/OnlyEditor.tsx
index b6fd1fc2f..368855622 100644
--- a/apps/examples/src/examples/only-editor/OnlyEditor.tsx
+++ b/apps/examples/src/examples/only-editor/OnlyEditor.tsx
@@ -1,6 +1,6 @@
/* eslint-disable import/no-extraneous-dependencies */
-import { Editor, PositionedOnCanvas, TldrawEditor, createShapeId, track } from '@tldraw/editor'
+import { Editor, TldrawEditor, createShapeId } from '@tldraw/editor'
import { MiniBoxShapeUtil } from './MiniBoxShape'
import { MiniSelectTool } from './MiniSelectTool'
@@ -32,24 +32,28 @@ export default function OnlyEditorExample() {
])
}}
components={{
- Background: BackgroundComponent,
+ // [3]
+ OnTheCanvas: () => {
+ return (
+
+
Double click to create or delete shapes.
+
Click or Shift+Click to select shapes.
+
Click and drag to move shapes.
+
+ )
+ },
}}
/>
)
}
-// [3]
-const BackgroundComponent = track(() => {
- return (
-