Composable custom UI (#2796)
This PR refactors our menu systems and provides an interface to hide or replace individual user interface elements. # Background Previously, we've had two types of overrides: - "schema" overrides that would allow insertion or replacement of items in the different menus - "component" overrides that would replace components in the editor's user interface This PR is an attempt to unify the two and to provide for additional cases where the "schema-based" user interface had begun to break down. # Approach This PR makes no attempt to change the `actions` or `tools` overrides—the current system seems to be correct for those because they are not reactive. The challenge with the other ui schemas is that they _are_ reactive, and thus the overrides both need to a) be fed in from outside of the editor as props, and b) react to changes from the editor, which is an impossible situation. The new approach is to use React to declare menu items. (Surprise!) ```tsx function CustomHelpMenuContent() { return ( <> <DefaultHelpMenuContent /> <TldrawUiMenuGroup id="custom stuff"> <TldrawUiMenuItem id="about" label="Like my posts" icon="external-link" readonlyOk onSelect={() => { window.open('https://x.com/tldraw', '_blank') }} /> </TldrawUiMenuGroup> </> ) } const components: TLComponents = { HelpMenuContent: CustomHelpMenuContent, } export default function CustomHelpMenuContentExample() { return ( <div className="tldraw__editor"> <Tldraw components={components} /> </div> ) } ``` We use a `components` prop with the combined editor and ui components. - [ ] Create a "layout" component? - [ ] Make UI components more isolated? If possible, they shouldn't depend on styles outside of themselves, so that they can be used in other layouts. Maybe we wait on this because I'm feeling a slippery slope toward presumptions about configurability. - [ ] OTOH maybe we go hard and consider these things as separate components, even packages, with their own interfaces for customizability / configurability, just go all the way with it, and see what that looks like. # Pros Top line: you can customize tldraw's user interface in a MUCH more granular / powerful way than before. It solves a case where menu items could not be made stateful from outside of the editor context, and provides the option to do things in the menus that we couldn't allow previously with the "schema-based" approach. It also may (who knows) be more performant because we can locate the state inside of the components for individual buttons and groups, instead of all at the top level above the "schema". Because items / groups decide their own state, we don't have to have big checks on how many items are selected, or whether we have a flippable state. Items and groups themselves are allowed to re-build as part of the regular React lifecycle. Menus aren't constantly being rebuilt, if that were ever an issue. Menu items can be shared between different menu types. We'll are sometimes able to re-use items between, for example, the menu and the context menu and the actions menu. Our overrides no longer mutate anything, so there's less weird searching and finding. # Cons This approach can make customization menu contents significantly more complex, as an end user would need to re-declare most of a menu in order to make any change to it. Luckily a user can add things to the top or bottom of the context menu fairly easily. (And who knows, folks may actually want to do deep customization, and this allows for it.) It's more code. We are shipping more react components, basically one for each menu item / group. Currently this PR does not export the subcomponents, i.e. menu items. If we do want to export these, then heaven help us, it's going to be a _lot_ of exports. # Progress - [x] Context menu - [x] Main menu - [x] Zoom menu - [x] Help menu - [x] Actions menu - [x] Keyboard shortcuts menu - [x] Quick actions in main menu? (new) - [x] Helper buttons? (new) - [x] Debug Menu And potentially - [x] Toolbar - [x] Style menu - [ ] Share zone - [x] Navigation zone - [ ] Other zones ### Change Type - [x] `major` — Breaking change ### Test Plan 1. use the context menu 2. use the custom context menu example 3. use cursor chat in the context menu - [x] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here.
This commit is contained in:
parent
5faac660bc
commit
ac0259a6af
189 changed files with 10501 additions and 7418 deletions
7
.ignore
7
.ignore
|
@ -19,3 +19,10 @@ apps/example/www/index.css
|
|||
apps/docs/.next
|
||||
|
||||
packages/tldraw/tldraw.css
|
||||
|
||||
**/dist-cjs/**/*
|
||||
**/dist-esm/**/*
|
||||
**/*.js.map
|
||||
**/*.api.json
|
||||
apps/docs/utils/vector-db
|
||||
packages/**/api
|
|
@ -87,7 +87,6 @@ const myOverrides: TLUiOverrides = {
|
|||
icon: 'color',
|
||||
label: 'tools.card',
|
||||
kbd: 'c',
|
||||
readonlyOk: false,
|
||||
onSelect: () => {
|
||||
// Whatever you want to happen when the tool is selected
|
||||
editor.setCurrentTool('card')
|
||||
|
|
|
@ -155,11 +155,11 @@ The [Tldraw](?) component combines two lower-level components: [TldrawEditor](?)
|
|||
|
||||
### Customize the default components
|
||||
|
||||
You can customize the appearance of the tldraw editor using the [Tldraw](?) (or [TldrawEditor](?) component's `components` prop.
|
||||
You can customize the appearance of the tldraw editor and ui using the [Tldraw](?) (or [TldrawEditor](?)) component's `components` prop.
|
||||
|
||||
```tsx
|
||||
<Tldraw
|
||||
components={{
|
||||
|
||||
const components: TLComponents = {
|
||||
Background: YourCustomBackground,
|
||||
SvgDefs: YourCustomSvgDefs,
|
||||
Brush: YourCustomBrush,
|
||||
|
@ -182,6 +182,8 @@ You can customize the appearance of the tldraw editor using the [Tldraw](?) (or
|
|||
SelectionBackground: YourCustomSelectionBackground,
|
||||
SelectionForeground: YourCustomSelectionForeground,
|
||||
HoveredShapeIndicator: YourCustomHoveredShapeIndicator,
|
||||
}}
|
||||
/>
|
||||
// ...
|
||||
}
|
||||
|
||||
<Tldraw components={components}/>
|
||||
```
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
import * as Popover from '@radix-ui/react-popover'
|
||||
import { Button, useActions, useContainer, useEditor, useTranslation } from '@tldraw/tldraw'
|
||||
import {
|
||||
TldrawUiMenuContextProvider,
|
||||
TldrawUiMenuGroup,
|
||||
TldrawUiMenuItem,
|
||||
unwrapLabel,
|
||||
useActions,
|
||||
useContainer,
|
||||
useEditor,
|
||||
useTranslation,
|
||||
} from '@tldraw/tldraw'
|
||||
import React, { useState } from 'react'
|
||||
import { useShareMenuIsOpen } from '../hooks/useShareMenuOpen'
|
||||
import { SHARE_PROJECT_ACTION, SHARE_SNAPSHOT_ACTION } from '../utils/sharing'
|
||||
|
@ -33,25 +42,20 @@ export const ExportMenu = React.memo(function ExportMenu() {
|
|||
side="bottom"
|
||||
sideOffset={6}
|
||||
>
|
||||
<div className="tlui-menu__group">
|
||||
<Button
|
||||
type="menu"
|
||||
label={shareProject.label}
|
||||
icon={'share-1'}
|
||||
onClick={() => {
|
||||
shareProject.onSelect('export-menu')
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuContextProvider type="panel" sourceId="export-menu">
|
||||
<TldrawUiMenuGroup id="share">
|
||||
<TldrawUiMenuItem {...shareProject} />
|
||||
<p className="tlui-menu__group tlui-share-zone__details">
|
||||
{msg('share-menu.fork-note')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="tlui-menu__group">
|
||||
<Button
|
||||
type="menu"
|
||||
</TldrawUiMenuGroup>
|
||||
<TldrawUiMenuGroup id="snapshot">
|
||||
<TldrawUiMenuItem
|
||||
id="copy-to-clipboard"
|
||||
readonlyOk
|
||||
icon={didCopySnapshotLink ? 'clipboard-copied' : 'clipboard-copy'}
|
||||
label={shareSnapshot.label!}
|
||||
onClick={async () => {
|
||||
label={unwrapLabel(shareSnapshot.label)}
|
||||
onSelect={async () => {
|
||||
setIsUploadingSnapshot(true)
|
||||
await shareSnapshot.onSelect('share-menu')
|
||||
setIsUploadingSnapshot(false)
|
||||
|
@ -63,20 +67,14 @@ export const ExportMenu = React.memo(function ExportMenu() {
|
|||
<p className="tlui-menu__group tlui-share-zone__details">
|
||||
{msg('share-menu.snapshot-link-note')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="tlui-menu__group">
|
||||
<Button
|
||||
type="menu"
|
||||
label={saveFileCopyAction.label}
|
||||
icon={'share-2'}
|
||||
onClick={() => {
|
||||
saveFileCopyAction.onSelect('export-menu')
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
<TldrawUiMenuGroup id="save">
|
||||
<TldrawUiMenuItem {...saveFileCopyAction} />
|
||||
<p className="tlui-menu__group tlui-share-zone__details">
|
||||
{msg('share-menu.save-note')}
|
||||
</p>
|
||||
</div>
|
||||
</TldrawUiMenuGroup>
|
||||
</TldrawUiMenuContextProvider>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
|
|
45
apps/dotcom/src/components/FileMenu.tsx
Normal file
45
apps/dotcom/src/components/FileMenu.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
TldrawUiMenuGroup,
|
||||
TldrawUiMenuItem,
|
||||
TldrawUiMenuSubmenu,
|
||||
useActions,
|
||||
} from '@tldraw/tldraw'
|
||||
import {
|
||||
FORK_PROJECT_ACTION,
|
||||
LEAVE_SHARED_PROJECT_ACTION,
|
||||
SHARE_PROJECT_ACTION,
|
||||
} from '../utils/sharing'
|
||||
import { NEW_PROJECT_ACTION, OPEN_FILE_ACTION, SAVE_FILE_COPY_ACTION } from '../utils/useFileSystem'
|
||||
|
||||
export function LocalFileMenu() {
|
||||
const actions = useActions()
|
||||
|
||||
return (
|
||||
<TldrawUiMenuSubmenu id="file" label="menu.file">
|
||||
<TldrawUiMenuGroup id="file-actions">
|
||||
<TldrawUiMenuItem {...actions[SAVE_FILE_COPY_ACTION]} />
|
||||
<TldrawUiMenuItem {...actions[OPEN_FILE_ACTION]} />
|
||||
<TldrawUiMenuItem {...actions[NEW_PROJECT_ACTION]} />
|
||||
</TldrawUiMenuGroup>
|
||||
<TldrawUiMenuGroup id="share">
|
||||
<TldrawUiMenuItem {...actions[SHARE_PROJECT_ACTION]} />
|
||||
</TldrawUiMenuGroup>
|
||||
</TldrawUiMenuSubmenu>
|
||||
)
|
||||
}
|
||||
|
||||
export function MultiplayerFileMenu() {
|
||||
const actions = useActions()
|
||||
|
||||
return (
|
||||
<TldrawUiMenuSubmenu id="file" label="menu.file">
|
||||
<TldrawUiMenuGroup id="file-actions">
|
||||
<TldrawUiMenuItem {...actions[SAVE_FILE_COPY_ACTION]} />
|
||||
</TldrawUiMenuGroup>
|
||||
<TldrawUiMenuGroup id="share">
|
||||
<TldrawUiMenuItem {...actions[FORK_PROJECT_ACTION]} />
|
||||
<TldrawUiMenuItem {...actions[LEAVE_SHARED_PROJECT_ACTION]} />
|
||||
</TldrawUiMenuGroup>
|
||||
</TldrawUiMenuSubmenu>
|
||||
)
|
||||
}
|
45
apps/dotcom/src/components/Links.tsx
Normal file
45
apps/dotcom/src/components/Links.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { TldrawUiMenuGroup, TldrawUiMenuItem } from '@tldraw/tldraw'
|
||||
import { openUrl } from '../utils/url'
|
||||
|
||||
export function Links() {
|
||||
return (
|
||||
<TldrawUiMenuGroup id="links">
|
||||
<TldrawUiMenuItem
|
||||
id="github"
|
||||
label="help-menu.github"
|
||||
readonlyOk
|
||||
icon="github"
|
||||
onSelect={() => {
|
||||
openUrl('https://github.com/tldraw/tldraw')
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="twitter"
|
||||
label="help-menu.twitter"
|
||||
icon="twitter"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl('https://twitter.com/tldraw')
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="discord"
|
||||
label="help-menu.discord"
|
||||
icon="discord"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl('https://discord.gg/SBBEVCA4PG')
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="about"
|
||||
label="help-menu.about"
|
||||
icon="external-link"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl('https://tldraw.dev')
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
)
|
||||
}
|
|
@ -1,21 +1,77 @@
|
|||
import { Editor, Tldraw } from '@tldraw/tldraw'
|
||||
import {
|
||||
DefaultDebugMenu,
|
||||
DefaultDebugMenuContent,
|
||||
DefaultHelpMenu,
|
||||
DefaultHelpMenuContent,
|
||||
DefaultKeyboardShortcutsDialog,
|
||||
DefaultKeyboardShortcutsDialogContent,
|
||||
DefaultMainMenu,
|
||||
DefaultMainMenuContent,
|
||||
Editor,
|
||||
TLComponents,
|
||||
Tldraw,
|
||||
TldrawUiMenuGroup,
|
||||
TldrawUiMenuItem,
|
||||
useActions,
|
||||
} from '@tldraw/tldraw'
|
||||
import { useCallback } from 'react'
|
||||
import { assetUrls } from '../utils/assetUrls'
|
||||
import { createAssetFromUrl } from '../utils/createAssetFromUrl'
|
||||
import { linksUiOverrides } from '../utils/links'
|
||||
import { DebugMenuItems } from '../utils/migration/DebugMenuItems'
|
||||
import { LocalMigration } from '../utils/migration/LocalMigration'
|
||||
import { SCRATCH_PERSISTENCE_KEY } from '../utils/scratch-persistence-key'
|
||||
import { useSharing } from '../utils/sharing'
|
||||
import { useFileSystem } from '../utils/useFileSystem'
|
||||
import { OPEN_FILE_ACTION, SAVE_FILE_COPY_ACTION, useFileSystem } from '../utils/useFileSystem'
|
||||
import { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
||||
import { LocalFileMenu } from './FileMenu'
|
||||
import { Links } from './Links'
|
||||
import { ShareMenu } from './ShareMenu'
|
||||
import { SneakyOnDropOverride } from './SneakyOnDropOverride'
|
||||
import { ThemeUpdater } from './ThemeUpdater/ThemeUpdater'
|
||||
|
||||
const components: TLComponents = {
|
||||
ErrorFallback: ({ error }) => {
|
||||
throw error
|
||||
},
|
||||
HelpMenu: () => (
|
||||
<DefaultHelpMenu>
|
||||
<TldrawUiMenuGroup id="help">
|
||||
<DefaultHelpMenuContent />
|
||||
</TldrawUiMenuGroup>
|
||||
<Links />
|
||||
</DefaultHelpMenu>
|
||||
),
|
||||
MainMenu: () => (
|
||||
<DefaultMainMenu>
|
||||
<LocalFileMenu />
|
||||
<DefaultMainMenuContent />
|
||||
</DefaultMainMenu>
|
||||
),
|
||||
KeyboardShortcutsDialog: (props) => {
|
||||
const actions = useActions()
|
||||
return (
|
||||
<DefaultKeyboardShortcutsDialog {...props}>
|
||||
<TldrawUiMenuGroup id="shortcuts-dialog.file">
|
||||
<TldrawUiMenuItem {...actions[SAVE_FILE_COPY_ACTION]} />
|
||||
<TldrawUiMenuItem {...actions[OPEN_FILE_ACTION]} />
|
||||
</TldrawUiMenuGroup>
|
||||
<DefaultKeyboardShortcutsDialogContent />
|
||||
</DefaultKeyboardShortcutsDialog>
|
||||
)
|
||||
},
|
||||
DebugMenu: () => {
|
||||
return (
|
||||
<DefaultDebugMenu>
|
||||
<DefaultDebugMenuContent />
|
||||
<DebugMenuItems />
|
||||
</DefaultDebugMenu>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export function LocalEditor() {
|
||||
const handleUiEvent = useHandleUiEvents()
|
||||
const sharingUiOverrides = useSharing({ isMultiplayer: false })
|
||||
const sharingUiOverrides = useSharing()
|
||||
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: false })
|
||||
|
||||
const handleMount = useCallback((editor: Editor) => {
|
||||
|
@ -29,19 +85,14 @@ export function LocalEditor() {
|
|||
persistenceKey={SCRATCH_PERSISTENCE_KEY}
|
||||
onMount={handleMount}
|
||||
autoFocus
|
||||
overrides={[sharingUiOverrides, fileSystemUiOverrides, linksUiOverrides]}
|
||||
overrides={[sharingUiOverrides, fileSystemUiOverrides]}
|
||||
onUiEvent={handleUiEvent}
|
||||
components={{
|
||||
ErrorFallback: ({ error }) => {
|
||||
throw error
|
||||
},
|
||||
}}
|
||||
components={components}
|
||||
shareZone={
|
||||
<div className="tlui-share-zone" draggable={false}>
|
||||
<ShareMenu />
|
||||
</div>
|
||||
}
|
||||
renderDebugMenuItems={() => <DebugMenuItems />}
|
||||
inferDarkMode
|
||||
>
|
||||
<LocalMigration />
|
||||
|
|
|
@ -1,25 +1,85 @@
|
|||
import { Editor, OfflineIndicator, Tldraw, lns } from '@tldraw/tldraw'
|
||||
import {
|
||||
DefaultContextMenu,
|
||||
DefaultContextMenuContent,
|
||||
DefaultHelpMenu,
|
||||
DefaultHelpMenuContent,
|
||||
DefaultKeyboardShortcutsDialog,
|
||||
DefaultKeyboardShortcutsDialogContent,
|
||||
DefaultMainMenu,
|
||||
DefaultMainMenuContent,
|
||||
Editor,
|
||||
OfflineIndicator,
|
||||
TLComponents,
|
||||
Tldraw,
|
||||
TldrawUiMenuGroup,
|
||||
TldrawUiMenuItem,
|
||||
lns,
|
||||
useActions,
|
||||
} from '@tldraw/tldraw'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useRemoteSyncClient } from '../hooks/useRemoteSyncClient'
|
||||
import { UrlStateParams, useUrlState } from '../hooks/useUrlState'
|
||||
import { assetUrls } from '../utils/assetUrls'
|
||||
import { MULTIPLAYER_SERVER } from '../utils/config'
|
||||
import { CursorChatMenuItem } from '../utils/context-menu/CursorChatMenuItem'
|
||||
import { createAssetFromFile } from '../utils/createAssetFromFile'
|
||||
import { createAssetFromUrl } from '../utils/createAssetFromUrl'
|
||||
import { linksUiOverrides } from '../utils/links'
|
||||
import { useSharing } from '../utils/sharing'
|
||||
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
|
||||
import { useCursorChat } from '../utils/useCursorChat'
|
||||
import { useFileSystem } from '../utils/useFileSystem'
|
||||
import { CURSOR_CHAT_ACTION, useCursorChat } from '../utils/useCursorChat'
|
||||
import { OPEN_FILE_ACTION, SAVE_FILE_COPY_ACTION, useFileSystem } from '../utils/useFileSystem'
|
||||
import { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
||||
import { CursorChatBubble } from './CursorChatBubble'
|
||||
import { EmbeddedInIFrameWarning } from './EmbeddedInIFrameWarning'
|
||||
import { MultiplayerFileMenu } from './FileMenu'
|
||||
import { Links } from './Links'
|
||||
import { PeopleMenu } from './PeopleMenu/PeopleMenu'
|
||||
import { ShareMenu } from './ShareMenu'
|
||||
import { SneakyOnDropOverride } from './SneakyOnDropOverride'
|
||||
import { StoreErrorScreen } from './StoreErrorScreen'
|
||||
import { ThemeUpdater } from './ThemeUpdater/ThemeUpdater'
|
||||
|
||||
const components: TLComponents = {
|
||||
ErrorFallback: ({ error }) => {
|
||||
throw error
|
||||
},
|
||||
ContextMenu: (props) => (
|
||||
<DefaultContextMenu {...props}>
|
||||
<CursorChatMenuItem />
|
||||
<DefaultContextMenuContent />
|
||||
</DefaultContextMenu>
|
||||
),
|
||||
HelpMenu: () => (
|
||||
<DefaultHelpMenu>
|
||||
<TldrawUiMenuGroup id="help">
|
||||
<DefaultHelpMenuContent />
|
||||
</TldrawUiMenuGroup>
|
||||
<Links />
|
||||
</DefaultHelpMenu>
|
||||
),
|
||||
MainMenu: () => (
|
||||
<DefaultMainMenu>
|
||||
<MultiplayerFileMenu />
|
||||
<DefaultMainMenuContent />
|
||||
</DefaultMainMenu>
|
||||
),
|
||||
KeyboardShortcutsDialog: (props) => {
|
||||
const actions = useActions()
|
||||
return (
|
||||
<DefaultKeyboardShortcutsDialog {...props}>
|
||||
<TldrawUiMenuGroup id="shortcuts-dialog.file">
|
||||
<TldrawUiMenuItem {...actions[SAVE_FILE_COPY_ACTION]} />
|
||||
<TldrawUiMenuItem {...actions[OPEN_FILE_ACTION]} />
|
||||
</TldrawUiMenuGroup>
|
||||
<DefaultKeyboardShortcutsDialogContent />
|
||||
<TldrawUiMenuGroup id="shortcuts-dialog.collaboration">
|
||||
<TldrawUiMenuItem {...actions[CURSOR_CHAT_ACTION]} />
|
||||
</TldrawUiMenuGroup>
|
||||
</DefaultKeyboardShortcutsDialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export function MultiplayerEditor({
|
||||
isReadOnly,
|
||||
roomSlug,
|
||||
|
@ -37,7 +97,7 @@ export function MultiplayerEditor({
|
|||
})
|
||||
|
||||
const isEmbedded = useIsEmbedded(roomSlug)
|
||||
const sharingUiOverrides = useSharing({ isMultiplayer: true })
|
||||
const sharingUiOverrides = useSharing()
|
||||
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
||||
const cursorChatOverrides = useCursorChat()
|
||||
|
||||
|
@ -67,19 +127,10 @@ export function MultiplayerEditor({
|
|||
store={storeWithStatus}
|
||||
assetUrls={assetUrls}
|
||||
onMount={handleMount}
|
||||
overrides={[sharingUiOverrides, fileSystemUiOverrides, cursorChatOverrides]}
|
||||
initialState={isReadOnly ? 'hand' : 'select'}
|
||||
overrides={[
|
||||
sharingUiOverrides,
|
||||
fileSystemUiOverrides,
|
||||
linksUiOverrides,
|
||||
cursorChatOverrides,
|
||||
]}
|
||||
onUiEvent={handleUiEvent}
|
||||
components={{
|
||||
ErrorFallback: ({ error }) => {
|
||||
throw error
|
||||
},
|
||||
}}
|
||||
components={components}
|
||||
topZone={isOffline && <OfflineIndicator />}
|
||||
shareZone={
|
||||
<div className="tlui-share-zone" draggable={false}>
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
import * as Popover from '@radix-ui/react-popover'
|
||||
import { Button, lns, useActions, useContainer, useTranslation } from '@tldraw/tldraw'
|
||||
import {
|
||||
TldrawUiMenuContextProvider,
|
||||
TldrawUiMenuGroup,
|
||||
TldrawUiMenuItem,
|
||||
lns,
|
||||
unwrapLabel,
|
||||
useActions,
|
||||
useContainer,
|
||||
useTranslation,
|
||||
} from '@tldraw/tldraw'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useShareMenuIsOpen } from '../hooks/useShareMenuOpen'
|
||||
import { createQRCodeImageDataString } from '../utils/qrcode'
|
||||
|
@ -105,6 +114,7 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
|||
sideOffset={2}
|
||||
alignOffset={4}
|
||||
>
|
||||
<TldrawUiMenuContextProvider type="panel" sourceId="share-menu">
|
||||
{shareState.state === 'shared' || shareState.state === 'readonly' ? (
|
||||
<>
|
||||
<button
|
||||
|
@ -119,23 +129,26 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
|||
navigator.clipboard.writeText(currentShareLinkUrl)
|
||||
}}
|
||||
/>
|
||||
<div className="tlui-menu__group">
|
||||
<Button
|
||||
type="menu"
|
||||
<TldrawUiMenuGroup id="copy">
|
||||
<TldrawUiMenuItem
|
||||
id="copy-to-clipboard"
|
||||
readonlyOk
|
||||
icon={didCopy ? 'clipboard-copied' : 'clipboard-copy'}
|
||||
label={isReadOnlyLink ? 'share-menu.copy-readonly-link' : 'share-menu.copy-link'}
|
||||
onClick={() => {
|
||||
label={
|
||||
isReadOnlyLink ? 'share-menu.copy-readonly-link' : 'share-menu.copy-link'
|
||||
}
|
||||
onSelect={() => {
|
||||
setDidCopy(true)
|
||||
setTimeout(() => setDidCopy(false), 750)
|
||||
navigator.clipboard.writeText(currentShareLinkUrl)
|
||||
}}
|
||||
/>
|
||||
{shareState.state === 'shared' && (
|
||||
<Button
|
||||
type="menu"
|
||||
<TldrawUiMenuItem
|
||||
id="toggle-read-only"
|
||||
label="share-menu.readonly-link"
|
||||
icon={isReadOnlyLink ? 'check' : 'checkbox-empty'}
|
||||
onClick={async () => {
|
||||
onSelect={async () => {
|
||||
setIsReadOnlyLink(() => !isReadOnlyLink)
|
||||
}}
|
||||
/>
|
||||
|
@ -147,14 +160,13 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
|||
: 'share-menu.copy-link-note'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</TldrawUiMenuGroup>
|
||||
|
||||
<div className="tlui-menu__group">
|
||||
<Button
|
||||
type="menu"
|
||||
<TldrawUiMenuGroup id="snapshot">
|
||||
<TldrawUiMenuItem
|
||||
{...shareSnapshot}
|
||||
icon={didCopySnapshotLink ? 'clipboard-copied' : 'clipboard-copy'}
|
||||
label={shareSnapshot.label!}
|
||||
onClick={async () => {
|
||||
onSelect={async () => {
|
||||
setIsUploadingSnapshot(true)
|
||||
await shareSnapshot.onSelect('share-menu')
|
||||
setIsUploadingSnapshot(false)
|
||||
|
@ -166,16 +178,16 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
|||
<p className="tlui-menu__group tlui-share-zone__details">
|
||||
{msg('share-menu.snapshot-link-note')}
|
||||
</p>
|
||||
</div>
|
||||
</TldrawUiMenuGroup>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="tlui-menu__group">
|
||||
<Button
|
||||
type="menu"
|
||||
<TldrawUiMenuGroup id="share">
|
||||
<TldrawUiMenuItem
|
||||
id="share-project"
|
||||
label="share-menu.share-project"
|
||||
icon="share-1"
|
||||
onClick={async () => {
|
||||
onSelect={async () => {
|
||||
if (isUploading) return
|
||||
setIsUploading(true)
|
||||
await shareProject.onSelect('menu')
|
||||
|
@ -192,13 +204,13 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
|||
: 'share-menu.copy-link-note'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="tlui-menu__group">
|
||||
<Button
|
||||
type="menu"
|
||||
</TldrawUiMenuGroup>
|
||||
<TldrawUiMenuGroup id="copy-snapshot-link">
|
||||
<TldrawUiMenuItem
|
||||
id="copy-snapshot-link"
|
||||
icon={didCopySnapshotLink ? 'clipboard-copied' : 'clipboard-copy'}
|
||||
label={shareSnapshot.label!}
|
||||
onClick={async () => {
|
||||
label={unwrapLabel(shareSnapshot.label)}
|
||||
onSelect={async () => {
|
||||
setIsUploadingSnapshot(true)
|
||||
await shareSnapshot.onSelect('share-menu')
|
||||
setIsUploadingSnapshot(false)
|
||||
|
@ -210,9 +222,10 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
|||
<p className="tlui-menu__group tlui-share-zone__details">
|
||||
{msg('share-menu.snapshot-link-note')}
|
||||
</p>
|
||||
</div>
|
||||
</TldrawUiMenuGroup>
|
||||
</>
|
||||
)}
|
||||
</TldrawUiMenuContextProvider>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
|
|
|
@ -1,14 +1,60 @@
|
|||
import { SerializedSchema, TLRecord, Tldraw } from '@tldraw/tldraw'
|
||||
import {
|
||||
DefaultHelpMenu,
|
||||
DefaultHelpMenuContent,
|
||||
DefaultKeyboardShortcutsDialog,
|
||||
DefaultKeyboardShortcutsDialogContent,
|
||||
DefaultMainMenu,
|
||||
DefaultMainMenuContent,
|
||||
SerializedSchema,
|
||||
TLComponents,
|
||||
TLRecord,
|
||||
Tldraw,
|
||||
TldrawUiMenuGroup,
|
||||
TldrawUiMenuItem,
|
||||
useActions,
|
||||
} from '@tldraw/tldraw'
|
||||
import { UrlStateSync } from '../components/MultiplayerEditor'
|
||||
import { StoreErrorScreen } from '../components/StoreErrorScreen'
|
||||
import { useLocalStore } from '../hooks/useLocalStore'
|
||||
import { assetUrls } from '../utils/assetUrls'
|
||||
import { linksUiOverrides } from '../utils/links'
|
||||
import { DebugMenuItems } from '../utils/migration/DebugMenuItems'
|
||||
import { useSharing } from '../utils/sharing'
|
||||
import { useFileSystem } from '../utils/useFileSystem'
|
||||
import { SAVE_FILE_COPY_ACTION, useFileSystem } from '../utils/useFileSystem'
|
||||
import { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
||||
import { ExportMenu } from './ExportMenu'
|
||||
import { MultiplayerFileMenu } from './FileMenu'
|
||||
import { Links } from './Links'
|
||||
|
||||
const components: TLComponents = {
|
||||
ErrorFallback: ({ error }) => {
|
||||
throw error
|
||||
},
|
||||
HelpMenu: () => (
|
||||
<DefaultHelpMenu>
|
||||
<TldrawUiMenuGroup id="help">
|
||||
<DefaultHelpMenuContent />
|
||||
</TldrawUiMenuGroup>
|
||||
<Links />
|
||||
</DefaultHelpMenu>
|
||||
),
|
||||
MainMenu: () => (
|
||||
<DefaultMainMenu>
|
||||
<MultiplayerFileMenu />
|
||||
<DefaultMainMenuContent />
|
||||
</DefaultMainMenu>
|
||||
),
|
||||
KeyboardShortcutsDialog: (props) => {
|
||||
const actions = useActions()
|
||||
return (
|
||||
<DefaultKeyboardShortcutsDialog {...props}>
|
||||
<TldrawUiMenuGroup id="shortcuts-dialog.file">
|
||||
<TldrawUiMenuItem {...actions[SAVE_FILE_COPY_ACTION]} />
|
||||
</TldrawUiMenuGroup>
|
||||
<DefaultKeyboardShortcutsDialogContent />
|
||||
</DefaultKeyboardShortcutsDialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
type SnapshotEditorProps = {
|
||||
schema: SerializedSchema
|
||||
|
@ -17,7 +63,7 @@ type SnapshotEditorProps = {
|
|||
|
||||
export function SnapshotsEditor(props: SnapshotEditorProps) {
|
||||
const handleUiEvent = useHandleUiEvents()
|
||||
const sharingUiOverrides = useSharing({ isMultiplayer: true })
|
||||
const sharingUiOverrides = useSharing()
|
||||
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
||||
const storeResult = useLocalStore(props.records, props.schema)
|
||||
if (!storeResult?.ok) return <StoreErrorScreen error={new Error(storeResult?.error)} />
|
||||
|
@ -27,16 +73,12 @@ export function SnapshotsEditor(props: SnapshotEditorProps) {
|
|||
<Tldraw
|
||||
assetUrls={assetUrls}
|
||||
store={storeResult.value}
|
||||
overrides={[sharingUiOverrides, fileSystemUiOverrides, linksUiOverrides]}
|
||||
overrides={[sharingUiOverrides, fileSystemUiOverrides]}
|
||||
onUiEvent={handleUiEvent}
|
||||
onMount={(editor) => {
|
||||
editor.updateInstanceState({ isReadonly: true })
|
||||
}}
|
||||
components={{
|
||||
ErrorFallback: ({ error }) => {
|
||||
throw error
|
||||
},
|
||||
}}
|
||||
components={components}
|
||||
shareZone={
|
||||
<div className="tlui-share-zone" draggable={false}>
|
||||
<ExportMenu />
|
||||
|
|
18
apps/dotcom/src/utils/context-menu/CursorChatMenuItem.tsx
Normal file
18
apps/dotcom/src/utils/context-menu/CursorChatMenuItem.tsx
Normal file
|
@ -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 <TldrawUiMenuItem {...actions[CURSOR_CHAT_ACTION]} />
|
||||
}
|
|
@ -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
|
||||
},
|
||||
}
|
|
@ -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 (
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item
|
||||
type="menu"
|
||||
onClick={() => {
|
||||
<TldrawUiMenuGroup id="release">
|
||||
<TldrawUiMenuItem
|
||||
id="release-info"
|
||||
title={`${RELEASE_INFO}`}
|
||||
label="Version"
|
||||
onSelect={() => {
|
||||
window.alert(`${RELEASE_INFO}`)
|
||||
}}
|
||||
title={`${RELEASE_INFO}`}
|
||||
>
|
||||
Version
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
type="menu"
|
||||
onClick={async () => {
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="v1"
|
||||
label="Test v1 content"
|
||||
onSelect={async () => {
|
||||
const { writeV1ContentsToIdb } = await import('./writeV1ContentsToIdb')
|
||||
await writeV1ContentsToIdb()
|
||||
window.location.reload()
|
||||
}}
|
||||
>
|
||||
Test v1 content
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{msg('file-system.confirm-clear.title')}</Dialog.Title>
|
||||
<Dialog.CloseButton />
|
||||
</Dialog.Header>
|
||||
<Dialog.Body style={{ maxWidth: 350 }}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{msg('file-system.confirm-clear.title')}</DialogTitle>
|
||||
<DialogCloseButton />
|
||||
</DialogHeader>
|
||||
<DialogBody style={{ maxWidth: 350 }}>
|
||||
{msg('file-system.confirm-clear.description')}
|
||||
</Dialog.Body>
|
||||
<Dialog.Footer className="tlui-dialog__footer__actions">
|
||||
</DialogBody>
|
||||
<DialogFooter className="tlui-dialog__footer__actions">
|
||||
<Button
|
||||
type="normal"
|
||||
onClick={() => setDontShowAgain(!dontShowAgain)}
|
||||
|
@ -70,7 +79,7 @@ function ConfirmClearDialog({
|
|||
>
|
||||
{msg('file-system.confirm-clear.continue')}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogCloseButton,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
TLUiDialogsContextType,
|
||||
useLocalStorageState,
|
||||
useTranslation,
|
||||
|
@ -46,14 +50,12 @@ function ConfirmLeaveDialog({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{msg('sharing.confirm-leave.title')}</Dialog.Title>
|
||||
<Dialog.CloseButton />
|
||||
</Dialog.Header>
|
||||
<Dialog.Body style={{ maxWidth: 350 }}>
|
||||
{msg('sharing.confirm-leave.description')}
|
||||
</Dialog.Body>
|
||||
<Dialog.Footer className="tlui-dialog__footer__actions">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{msg('sharing.confirm-leave.title')}</DialogTitle>
|
||||
<DialogCloseButton />
|
||||
</DialogHeader>
|
||||
<DialogBody style={{ maxWidth: 350 }}>{msg('sharing.confirm-leave.description')}</DialogBody>
|
||||
<DialogFooter className="tlui-dialog__footer__actions">
|
||||
<Button
|
||||
type="normal"
|
||||
onClick={() => setDontShowAgain(!dontShowAgain)}
|
||||
|
@ -76,7 +78,7 @@ function ConfirmLeaveDialog({
|
|||
>
|
||||
{msg('sharing.confirm-leave.leave')}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{msg('file-system.confirm-open.title')}</Dialog.Title>
|
||||
<Dialog.CloseButton />
|
||||
</Dialog.Header>
|
||||
<Dialog.Body style={{ maxWidth: 350 }}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{msg('file-system.confirm-open.title')}</DialogTitle>
|
||||
<DialogCloseButton />
|
||||
</DialogHeader>
|
||||
<DialogBody style={{ maxWidth: 350 }}>
|
||||
{msg('file-system.confirm-open.description')}
|
||||
</Dialog.Body>
|
||||
<Dialog.Footer className="tlui-dialog__footer__actions">
|
||||
</DialogBody>
|
||||
<DialogFooter className="tlui-dialog__footer__actions">
|
||||
<Button
|
||||
type="normal"
|
||||
onClick={() => setDontShowAgain(!dontShowAgain)}
|
||||
|
@ -70,7 +79,7 @@ function ConfirmOpenDialog({
|
|||
>
|
||||
{msg('file-system.confirm-open.open')}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
3
apps/dotcom/src/utils/url.ts
Normal file
3
apps/dotcom/src/utils/url.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function openUrl(url: string) {
|
||||
window.open(url, '_blank')
|
||||
}
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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<TLStore, string>()
|
||||
|
||||
|
@ -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])
|
||||
}
|
||||
|
|
42
apps/examples/e2e/tests/context-menu.spec.ts
Normal file
42
apps/examples/e2e/tests/context-menu.spec.ts
Normal file
|
@ -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' },
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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',
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<img src="${path}.svg" />
|
||||
`,
|
||||
'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',
|
||||
// `
|
||||
// <!DOCTYPE html>
|
||||
// <meta charset="utf-8" />
|
||||
// <meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
// <img src="${path}.svg" />
|
||||
// `,
|
||||
// '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
|
||||
// }
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { DefaultActionsMenu, TLComponents, Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
function CustomActionsMenu() {
|
||||
return (
|
||||
<div style={{ transform: 'rotate(90deg)' }}>
|
||||
<DefaultActionsMenu />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
ActionsMenu: CustomActionsMenu,
|
||||
}
|
||||
|
||||
export default function CustomActionsMenuExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-actions-menu/README.md
Normal file
11
apps/examples/src/examples/custom-actions-menu/README.md
Normal file
|
@ -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.
|
|
@ -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 (
|
||||
<DefaultKeyboardShortcutsDialog {...props}>
|
||||
<DefaultKeyboardShortcutsDialogContent />
|
||||
{/* Ideally, we'd interleave this into the tools group */}
|
||||
<TldrawUiMenuItem {...tools['card']} />
|
||||
</DefaultKeyboardShortcutsDialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<DefaultContextMenu {...props}>
|
||||
<TldrawUiMenuGroup id="example">
|
||||
<TldrawUiMenuItem
|
||||
id="like"
|
||||
label="Like my posts"
|
||||
icon="external-link"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
window.open('https://x.com/tldraw', '_blank')
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
<DefaultContextMenuContent />
|
||||
</DefaultContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
ContextMenu: CustomContextMenu,
|
||||
}
|
||||
|
||||
export default function CustomContextMenuExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-context-menu/README.md
Normal file
11
apps/examples/src/examples/custom-context-menu/README.md
Normal file
|
@ -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.
|
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
DefaultDebugMenu,
|
||||
DefaultDebugMenuContent,
|
||||
TLComponents,
|
||||
Tldraw,
|
||||
TldrawUiMenuGroup,
|
||||
TldrawUiMenuItem,
|
||||
} from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
function CustomDebugMenu() {
|
||||
return (
|
||||
<DefaultDebugMenu>
|
||||
<DefaultDebugMenuContent />
|
||||
<TldrawUiMenuGroup id="example">
|
||||
<TldrawUiMenuItem
|
||||
id="like"
|
||||
label="Like my posts"
|
||||
icon="external-link"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
window.open('https://x.com/tldraw', '_blank')
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
</DefaultDebugMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
DebugMenu: CustomDebugMenu,
|
||||
}
|
||||
|
||||
export default function CustomDebugMenuExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-debug-menu/README.md
Normal file
11
apps/examples/src/examples/custom-debug-menu/README.md
Normal file
|
@ -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.
|
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
DefaultHelpMenu,
|
||||
DefaultHelpMenuContent,
|
||||
TLComponents,
|
||||
Tldraw,
|
||||
TldrawUiMenuGroup,
|
||||
TldrawUiMenuItem,
|
||||
} from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
function CustomHelpMenu() {
|
||||
return (
|
||||
<DefaultHelpMenu>
|
||||
<TldrawUiMenuGroup id="example">
|
||||
<TldrawUiMenuItem
|
||||
id="like"
|
||||
label="Like my posts"
|
||||
icon="external-link"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
window.open('https://x.com/tldraw', '_blank')
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
<DefaultHelpMenuContent />
|
||||
</DefaultHelpMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
HelpMenu: CustomHelpMenu,
|
||||
}
|
||||
|
||||
export default function CustomHelpMenuExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-help-menu/README.md
Normal file
11
apps/examples/src/examples/custom-help-menu/README.md
Normal file
|
@ -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.
|
|
@ -0,0 +1,38 @@
|
|||
import {
|
||||
DefaultKeyboardShortcutsDialog,
|
||||
DefaultKeyboardShortcutsDialogContent,
|
||||
TLComponents,
|
||||
TLUiKeyboardShortcutsDialogProps,
|
||||
Tldraw,
|
||||
TldrawUiMenuItem,
|
||||
} from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
function CustomKeyboardShortcutsDialog(props: TLUiKeyboardShortcutsDialogProps) {
|
||||
return (
|
||||
<DefaultKeyboardShortcutsDialog {...props}>
|
||||
<TldrawUiMenuItem
|
||||
id="about"
|
||||
label="Like my posts"
|
||||
icon="external-link"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
window.open('https://x.com/tldraw', '_blank')
|
||||
}}
|
||||
/>
|
||||
<DefaultKeyboardShortcutsDialogContent />
|
||||
</DefaultKeyboardShortcutsDialog>
|
||||
)
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
KeyboardShortcutsDialog: CustomKeyboardShortcutsDialog,
|
||||
}
|
||||
|
||||
export default function CustomKeyboardShortcutsDialogExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -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.
|
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
DefaultMainMenu,
|
||||
DefaultMainMenuContent,
|
||||
TLComponents,
|
||||
Tldraw,
|
||||
TldrawUiMenuGroup,
|
||||
TldrawUiMenuItem,
|
||||
} from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
function CustomMainMenu() {
|
||||
return (
|
||||
<DefaultMainMenu>
|
||||
<TldrawUiMenuGroup id="example">
|
||||
<TldrawUiMenuItem
|
||||
id="like"
|
||||
label="Like my posts"
|
||||
icon="external-link"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
window.open('https://x.com/tldraw', '_blank')
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
<DefaultMainMenuContent />
|
||||
</DefaultMainMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
MainMenu: CustomMainMenu,
|
||||
}
|
||||
|
||||
export default function CustomMainMenuExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-main-menu/README.md
Normal file
11
apps/examples/src/examples/custom-main-menu/README.md
Normal file
|
@ -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.
|
|
@ -0,0 +1,18 @@
|
|||
import { TLComponents, Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
function CustomNavigationPanel() {
|
||||
return <div className="tlui-navigation-panel">here you are</div>
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
NavigationPanel: CustomNavigationPanel, // null will hide the panel instead
|
||||
}
|
||||
|
||||
export default function CustomNagiationPanelExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-navigation-panel/README.md
Normal file
11
apps/examples/src/examples/custom-navigation-panel/README.md
Normal file
|
@ -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.
|
|
@ -0,0 +1,22 @@
|
|||
import { DefaultPageMenu, TLComponents, Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
function CustomPageMenu() {
|
||||
return (
|
||||
<div style={{ transform: 'rotate(3.14rad)' }}>
|
||||
<DefaultPageMenu />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
PageMenu: CustomPageMenu, // null will hide the page menu instead
|
||||
}
|
||||
|
||||
export default function CustomPageMenuExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-page-menu/README.md
Normal file
11
apps/examples/src/examples/custom-page-menu/README.md
Normal file
|
@ -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.
|
|
@ -0,0 +1,29 @@
|
|||
import {
|
||||
Button,
|
||||
DefaultQuickActions,
|
||||
DefaultQuickActionsContent,
|
||||
TLComponents,
|
||||
Tldraw,
|
||||
} from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
function CustomQuickActions() {
|
||||
return (
|
||||
<DefaultQuickActions>
|
||||
<DefaultQuickActionsContent />
|
||||
<Button type="icon" icon="code" smallIcon />
|
||||
</DefaultQuickActions>
|
||||
)
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
QuickActions: CustomQuickActions, // null will hide the page menu instead
|
||||
}
|
||||
|
||||
export default function CustomQuickActionsExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-quick-actions/README.md
Normal file
11
apps/examples/src/examples/custom-quick-actions/README.md
Normal file
|
@ -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.
|
|
@ -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 (
|
||||
<DefaultStylePanel {...props}>
|
||||
<Button
|
||||
type="menu"
|
||||
onClick={() => {
|
||||
editor.setStyleForSelectedShapes(DefaultColorStyle, 'red', { squashing: true })
|
||||
}}
|
||||
>
|
||||
Red
|
||||
</Button>
|
||||
<Button
|
||||
type="menu"
|
||||
onClick={() => {
|
||||
editor.setStyleForSelectedShapes(DefaultColorStyle, 'green', { squashing: true })
|
||||
}}
|
||||
>
|
||||
Green
|
||||
</Button>
|
||||
<DefaultStylePanelContent relevantStyles={props.relevantStyles} />
|
||||
</DefaultStylePanel>
|
||||
)
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
StylePanel: CustomStylePanel, // null will hide the panel instead
|
||||
}
|
||||
|
||||
export default function CustomStylePanelExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-style-panel/README.md
Normal file
11
apps/examples/src/examples/custom-style-panel/README.md
Normal file
|
@ -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.
|
|
@ -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}
|
||||
>
|
||||
<FilterStyleUi />
|
||||
</Tldraw>
|
||||
|
|
|
@ -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 (
|
||||
<DefaultKeyboardShortcutsDialog {...props}>
|
||||
<DefaultKeyboardShortcutsDialogContent />
|
||||
{/* Ideally, we'd interleave this into the tools section */}
|
||||
<TldrawUiMenuItem {...tools['card']} />
|
||||
</DefaultKeyboardShortcutsDialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div style={{ transform: 'rotate(180deg)' }}>
|
||||
<DefaultToolbar />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
Toolbar: CustomToolbar, // null will hide the panel instead
|
||||
}
|
||||
|
||||
export default function CustomToolbarExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-toolbar/README.md
Normal file
11
apps/examples/src/examples/custom-toolbar/README.md
Normal file
|
@ -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.
|
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
DefaultZoomMenu,
|
||||
DefaultZoomMenuContent,
|
||||
TLComponents,
|
||||
Tldraw,
|
||||
TldrawUiMenuGroup,
|
||||
TldrawUiMenuItem,
|
||||
} from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
function CustomZoomMenu() {
|
||||
return (
|
||||
<DefaultZoomMenu>
|
||||
<TldrawUiMenuGroup id="example">
|
||||
<TldrawUiMenuItem
|
||||
id="like"
|
||||
label="Like my posts"
|
||||
icon="external-link"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
window.open('https://x.com/tldraw', '_blank')
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
<DefaultZoomMenuContent />
|
||||
</DefaultZoomMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
ZoomMenu: CustomZoomMenu,
|
||||
}
|
||||
|
||||
export default function CustomZoomMenuExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-zoom-menu/README.md
Normal file
11
apps/examples/src/examples/custom-zoom-menu/README.md
Normal file
|
@ -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.
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
Canvas,
|
||||
ContextMenu,
|
||||
DefaultContextMenuContent,
|
||||
TldrawEditor,
|
||||
TldrawHandles,
|
||||
TldrawHoveredShapeIndicator,
|
||||
|
@ -38,8 +39,8 @@ export default function ExplodedExample() {
|
|||
persistenceKey="exploded-example"
|
||||
>
|
||||
<TldrawUi>
|
||||
<ContextMenu>
|
||||
<Canvas />
|
||||
<ContextMenu canvas={<Canvas />}>
|
||||
<DefaultContextMenuContent />
|
||||
</ContextMenu>
|
||||
</TldrawUi>
|
||||
</TldrawEditor>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(16px, 16px)`,
|
||||
width: '320px',
|
||||
}}
|
||||
>
|
||||
<p>Double click to create or delete shapes.</p>
|
||||
<p>Click or Shift+Click to select shapes.</p>
|
||||
<p>Click and drag to move shapes.</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// [3]
|
||||
const BackgroundComponent = track(() => {
|
||||
return (
|
||||
<PositionedOnCanvas x={16} y={16}>
|
||||
<p>Double click to create or delete shapes.</p>
|
||||
<p>Click or Shift+Click to select shapes.</p>
|
||||
<p>Click and drag to move shapes.</p>
|
||||
</PositionedOnCanvas>
|
||||
)
|
||||
})
|
||||
|
||||
/*
|
||||
This example shows how to use the TldrawEditor component on its own. This is useful if you want to
|
||||
create your own custom UI, shape and tool interactions.
|
||||
|
|
|
@ -26,7 +26,6 @@ const customUiOverrides: TLUiOverrides = {
|
|||
screenshot: {
|
||||
id: 'screenshot',
|
||||
label: 'Screenshot',
|
||||
readonlyOk: false,
|
||||
icon: 'tool-screenshot',
|
||||
kbd: 'j',
|
||||
onSelect() {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Tldraw } from '@tldraw/tldraw'
|
|||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { SpeechBubbleTool } from './SpeechBubble/SpeechBubbleTool'
|
||||
import { SpeechBubbleUtil } from './SpeechBubble/SpeechBubbleUtil'
|
||||
import { customAssetUrls, uiOverrides } from './SpeechBubble/ui-overrides'
|
||||
import { components, customAssetUrls, uiOverrides } from './SpeechBubble/ui-overrides'
|
||||
import './customhandles.css'
|
||||
|
||||
// There's a guide at the bottom of this file!
|
||||
|
@ -20,6 +20,7 @@ export default function CustomShapeWithHandles() {
|
|||
tools={tools}
|
||||
overrides={uiOverrides}
|
||||
assetUrls={customAssetUrls}
|
||||
components={components}
|
||||
persistenceKey="whatever"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import {
|
||||
DefaultKeyboardShortcutsDialog,
|
||||
DefaultKeyboardShortcutsDialogContent,
|
||||
TLComponents,
|
||||
TLUiAssetUrlOverrides,
|
||||
TLUiMenuGroup,
|
||||
TLUiOverrides,
|
||||
menuItem,
|
||||
TldrawUiMenuItem,
|
||||
toolbarItem,
|
||||
useTools,
|
||||
} from '@tldraw/tldraw'
|
||||
|
||||
// There's a guide at the bottom of this file!
|
||||
|
@ -16,7 +19,6 @@ export const uiOverrides: TLUiOverrides = {
|
|||
icon: 'speech-bubble',
|
||||
label: 'Speech Bubble',
|
||||
kbd: 's',
|
||||
readonlyOk: false,
|
||||
onSelect: () => {
|
||||
editor.setCurrentTool('speech-bubble')
|
||||
},
|
||||
|
@ -27,13 +29,6 @@ export const uiOverrides: TLUiOverrides = {
|
|||
toolbar.splice(4, 0, toolbarItem(tools.speech))
|
||||
return toolbar
|
||||
},
|
||||
keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
|
||||
const toolsGroup = keyboardShortcutsMenu.find(
|
||||
(group) => group.id === 'shortcuts-dialog.tools'
|
||||
) as TLUiMenuGroup
|
||||
toolsGroup.children.push(menuItem(tools.speech))
|
||||
return keyboardShortcutsMenu
|
||||
},
|
||||
}
|
||||
|
||||
// [2]
|
||||
|
@ -44,6 +39,18 @@ export const customAssetUrls: TLUiAssetUrlOverrides = {
|
|||
},
|
||||
}
|
||||
|
||||
export const components: TLComponents = {
|
||||
KeyboardShortcutsDialog: (props) => {
|
||||
const tools = useTools()
|
||||
return (
|
||||
<DefaultKeyboardShortcutsDialog {...props}>
|
||||
<DefaultKeyboardShortcutsDialogContent />
|
||||
<TldrawUiMenuItem {...tools['speech']} />
|
||||
</DefaultKeyboardShortcutsDialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
This file contains overrides for the Tldraw UI. These overrides are used to add your custom tools
|
11
apps/examples/src/examples/ui-components-hidden/README.md
Normal file
11
apps/examples/src/examples/ui-components-hidden/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: Hidden UI components
|
||||
component: ./UiComponentsHiddenExample.tsx
|
||||
category: ui
|
||||
---
|
||||
|
||||
You can hide tldraw's UI components.
|
||||
|
||||
---
|
||||
|
||||
Custom UI components can be hidden providing a `null` as the value for a component in `uiComponents`. In this case, all configurable UI components are hidden.
|
|
@ -0,0 +1,28 @@
|
|||
import { TLUiComponents, Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
// The type here is include only to ensure this example contains all possible ui components,
|
||||
const components: Required<TLUiComponents> = {
|
||||
ContextMenu: null,
|
||||
ActionsMenu: null,
|
||||
HelpMenu: null,
|
||||
ZoomMenu: null,
|
||||
MainMenu: null,
|
||||
Minimap: null,
|
||||
StylePanel: null,
|
||||
PageMenu: null,
|
||||
NavigationPanel: null,
|
||||
Toolbar: null,
|
||||
KeyboardShortcutsDialog: null,
|
||||
QuickActions: null,
|
||||
HelperButtons: null,
|
||||
DebugMenu: null,
|
||||
}
|
||||
|
||||
export default function UiComponentsHiddenExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
45
apps/vscode/editor/src/Links.tsx
Normal file
45
apps/vscode/editor/src/Links.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { TldrawUiMenuGroup, TldrawUiMenuItem } from '@tldraw/tldraw'
|
||||
import { openUrl } from './utils/url'
|
||||
|
||||
export function Links() {
|
||||
return (
|
||||
<TldrawUiMenuGroup id="links">
|
||||
<TldrawUiMenuItem
|
||||
id="github"
|
||||
label="help-menu.github"
|
||||
readonlyOk
|
||||
icon="github"
|
||||
onSelect={() => {
|
||||
openUrl('https://github.com/tldraw/tldraw')
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="twitter"
|
||||
label="help-menu.twitter"
|
||||
icon="twitter"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl('https://twitter.com/tldraw')
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="discord"
|
||||
label="help-menu.discord"
|
||||
icon="discord"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl('https://discord.gg/SBBEVCA4PG')
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="about"
|
||||
label="help-menu.about"
|
||||
icon="external-link"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl('https://tldraw.dev')
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
)
|
||||
}
|
|
@ -1,15 +1,24 @@
|
|||
import { linksUiOverrides } from './utils/links'
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
import { getAssetUrlsByImport } from '@tldraw/assets/imports'
|
||||
import { Editor, ErrorBoundary, TLUiMenuSchema, Tldraw, setRuntimeOverrides } from '@tldraw/tldraw'
|
||||
import {
|
||||
DefaultHelpMenu,
|
||||
DefaultHelpMenuContent,
|
||||
Editor,
|
||||
ErrorBoundary,
|
||||
TLComponents,
|
||||
Tldraw,
|
||||
TldrawUiMenuGroup,
|
||||
setRuntimeOverrides,
|
||||
} from '@tldraw/tldraw'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { VscodeMessage } from '../../messages'
|
||||
import '../public/index.css'
|
||||
import { ChangeResponder } from './ChangeResponder'
|
||||
import { FileOpen } from './FileOpen'
|
||||
import { FullPageMessage } from './FullPageMessage'
|
||||
import { Links } from './Links'
|
||||
import { onCreateAssetFromUrl } from './utils/bookmarks'
|
||||
import { vscode } from './utils/vscode'
|
||||
|
||||
|
@ -53,24 +62,6 @@ export function WrappedTldrawEditor() {
|
|||
)
|
||||
}
|
||||
|
||||
const menuOverrides = {
|
||||
menu: (_editor: Editor, schema: TLUiMenuSchema, _helpers: any) => {
|
||||
schema.forEach((item) => {
|
||||
if (item.id === 'menu' && item.type === 'group') {
|
||||
item.children = item.children.filter((menuItem) => {
|
||||
if (!menuItem) return false
|
||||
if (menuItem.id === 'file' && menuItem.type === 'submenu') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return schema
|
||||
},
|
||||
}
|
||||
|
||||
export const TldrawWrapper = () => {
|
||||
const [tldrawInnerProps, setTldrawInnerProps] = useState<TLDrawInnerProps | null>(null)
|
||||
|
||||
|
@ -114,6 +105,16 @@ export type TLDrawInnerProps = {
|
|||
isDarkMode: boolean
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
HelpMenu: () => (
|
||||
<DefaultHelpMenu>
|
||||
<TldrawUiMenuGroup id="help">
|
||||
<DefaultHelpMenuContent />
|
||||
</TldrawUiMenuGroup>
|
||||
<Links />
|
||||
</DefaultHelpMenu>
|
||||
),
|
||||
}
|
||||
function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerProps) {
|
||||
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
|
||||
|
||||
|
@ -126,10 +127,11 @@ function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerPro
|
|||
assetUrls={assetUrls}
|
||||
persistenceKey={uri}
|
||||
onMount={handleMount}
|
||||
overrides={[menuOverrides, linksUiOverrides]}
|
||||
components={components}
|
||||
autoFocus
|
||||
>
|
||||
{/* <DarkModeHandler themeKind={themeKind} /> */}
|
||||
|
||||
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
|
||||
<ChangeResponder />
|
||||
</Tldraw>
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
import { menuGroup, menuItem, TLUiOverrides } from '@tldraw/tldraw'
|
||||
import { openUrl } from './openUrl'
|
||||
|
||||
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() {
|
||||
openUrl(GITHUB_URL)
|
||||
},
|
||||
}),
|
||||
menuItem({
|
||||
id: 'twitter',
|
||||
label: 'help-menu.twitter',
|
||||
icon: 'twitter',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
openUrl('https://twitter.com/tldraw')
|
||||
},
|
||||
}),
|
||||
menuItem({
|
||||
id: 'discord',
|
||||
label: 'help-menu.discord',
|
||||
icon: 'discord',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
openUrl('https://discord.gg/SBBEVCA4PG')
|
||||
},
|
||||
}),
|
||||
menuItem({
|
||||
id: 'about',
|
||||
label: 'help-menu.about',
|
||||
icon: 'external-link',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
openUrl('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
|
||||
},
|
||||
}
|
|
@ -332,7 +332,7 @@
|
|||
"action.toggle-reduce-motion.menu": "Zmanjšaj gibanje",
|
||||
"action.toggle-reduce-motion": "Preklop zmanjšanja gibanja",
|
||||
"tool.highlight": "Marker",
|
||||
"action.toggle-lock": "Zakleni \/ odkleni",
|
||||
"action.toggle-lock": "Zakleni / odkleni",
|
||||
"share-menu.default-project-name": "Skupni projekt",
|
||||
"home-project-dialog.title": "Lokalni projekt",
|
||||
"home-project-dialog.description": "To je vaš lokalni projekt. Namenjen je samo vam!",
|
||||
|
|
|
@ -16,11 +16,9 @@ import { EmbedDefinition } from '@tldraw/tlschema';
|
|||
import { EMPTY_ARRAY } from '@tldraw/state';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { HistoryEntry } from '@tldraw/store';
|
||||
import { HTMLProps } from 'react';
|
||||
import { IndexKey } from '@tldraw/utils';
|
||||
import { JsonObject } from '@tldraw/utils';
|
||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||
import { MemoExoticComponent } from 'react';
|
||||
import { Migrations } from '@tldraw/store';
|
||||
import { NamedExoticComponent } from 'react';
|
||||
import { PointerEventHandler } from 'react';
|
||||
|
@ -426,22 +424,7 @@ export function dataUrlToFile(url: string, filename: string, mimeType: string):
|
|||
export type DebugFlag<T> = DebugFlagDef<T> & Atom<T>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const debugFlags: {
|
||||
preventDefaultLogging: DebugFlag<boolean>;
|
||||
pointerCaptureLogging: DebugFlag<boolean>;
|
||||
pointerCaptureTracking: DebugFlag<boolean>;
|
||||
pointerCaptureTrackingObject: DebugFlag<Map<Element, number>>;
|
||||
elementRemovalLogging: DebugFlag<boolean>;
|
||||
debugSvg: DebugFlag<boolean>;
|
||||
showFps: DebugFlag<boolean>;
|
||||
throwToBlob: DebugFlag<boolean>;
|
||||
logMessages: DebugFlag<any[]>;
|
||||
resetConnectionEveryPing: DebugFlag<boolean>;
|
||||
debugCursors: DebugFlag<boolean>;
|
||||
forceSrgb: DebugFlag<boolean>;
|
||||
debugGeometry: DebugFlag<boolean>;
|
||||
hideShapes: DebugFlag<boolean>;
|
||||
};
|
||||
export const debugFlags: Record<string, DebugFlag<boolean>>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DEFAULT_ANIMATION_OPTIONS: {
|
||||
|
@ -1452,13 +1435,6 @@ export class Polyline2d extends Geometry2d {
|
|||
_segments?: Edge2d[];
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const PositionedOnCanvas: MemoExoticComponent<({ x: offsetX, y: offsetY, rotation, ...rest }: {
|
||||
x?: number | undefined;
|
||||
y?: number | undefined;
|
||||
rotation?: number | undefined;
|
||||
} & HTMLProps<HTMLDivElement>) => JSX_2.Element>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function precise(A: VecLike): string;
|
||||
|
||||
|
|
|
@ -27915,83 +27915,6 @@
|
|||
},
|
||||
"implementsTokenRanges": []
|
||||
},
|
||||
{
|
||||
"kind": "Variable",
|
||||
"canonicalReference": "@tldraw/editor!PositionedOnCanvas:var",
|
||||
"docComment": "/**\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "PositionedOnCanvas: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "import(\"react\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "MemoExoticComponent",
|
||||
"canonicalReference": "@types/react!React.MemoExoticComponent:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<({ "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "x",
|
||||
"canonicalReference": "@tldraw/editor!~__type#x"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ": offsetX, "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "y",
|
||||
"canonicalReference": "@tldraw/editor!~__type#y"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ": offsetY, rotation, ...rest }: {\n x?: number | undefined;\n y?: number | undefined;\n rotation?: number | undefined;\n} & "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "HTMLProps",
|
||||
"canonicalReference": "@types/react!React.HTMLProps:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<"
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "HTMLDivElement",
|
||||
"canonicalReference": "!HTMLDivElement:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">) => import(\"react/jsx-runtime\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "JSX.Element",
|
||||
"canonicalReference": "@types/react!JSX.Element:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/editor/src/lib/components/PositionedOnCanvas.tsx",
|
||||
"isReadonly": true,
|
||||
"releaseTag": "Public",
|
||||
"name": "PositionedOnCanvas",
|
||||
"variableTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 14
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Function",
|
||||
"canonicalReference": "@tldraw/editor!precise:function(1)",
|
||||
|
|
|
@ -41,7 +41,6 @@ export {
|
|||
type TLErrorBoundaryProps,
|
||||
} from './lib/components/ErrorBoundary'
|
||||
export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
|
||||
export { PositionedOnCanvas } from './lib/components/PositionedOnCanvas'
|
||||
export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
|
||||
export { ShapeIndicator, type TLShapeIndicatorComponent } from './lib/components/ShapeIndicator'
|
||||
export {
|
||||
|
|
|
@ -99,7 +99,6 @@ export function Canvas({ className }: { className?: string }) {
|
|||
>
|
||||
{Background && <Background />}
|
||||
<GridWrapper />
|
||||
<UiLogger />
|
||||
<svg className="tl-svg-context">
|
||||
<defs>
|
||||
{shapeSvgDefs}
|
||||
|
@ -511,26 +510,6 @@ const DebugSvgCopy = track(function DupSvg({ id }: { id: TLShapeId }) {
|
|||
)
|
||||
})
|
||||
|
||||
function UiLogger() {
|
||||
const uiLog = useValue('debugging ui log', () => debugFlags.logMessages.get(), [debugFlags])
|
||||
|
||||
if (!uiLog.length) return null
|
||||
|
||||
return (
|
||||
<div className="debug__ui-logger">
|
||||
{uiLog.map((message, messageIndex) => {
|
||||
const text = typeof message === 'string' ? message : JSON.stringify(message)
|
||||
|
||||
return (
|
||||
<div className="debug__ui-logger__line" key={messageIndex}>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectionForegroundWrapper() {
|
||||
const editor = useEditor()
|
||||
const selectionRotation = useValue('selection rotation', () => editor.getSelectionRotation(), [
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import { track } from '@tldraw/state'
|
||||
import classNames from 'classnames'
|
||||
import { HTMLProps, useLayoutEffect, useRef } from 'react'
|
||||
import { useEditor } from '../hooks/useEditor'
|
||||
|
||||
/** @public */
|
||||
export const PositionedOnCanvas = track(function PositionedOnCanvas({
|
||||
x: offsetX = 0,
|
||||
y: offsetY = 0,
|
||||
rotation = 0,
|
||||
...rest
|
||||
}: {
|
||||
x?: number
|
||||
y?: number
|
||||
rotation?: number
|
||||
} & HTMLProps<HTMLDivElement>) {
|
||||
const editor = useEditor()
|
||||
const rContainer = useRef<HTMLDivElement>(null)
|
||||
const camera = editor.getCamera()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const { x, y, z } = editor.getCamera()
|
||||
const elm = rContainer.current
|
||||
if (!elm) return
|
||||
if (x === undefined) return
|
||||
|
||||
elm.style.transform = `translate(${x}px, ${y}px) scale(${z}) rotate(${rotation}rad) translate(${offsetX}px, ${offsetY}px)`
|
||||
}, [camera, editor, offsetX, offsetY, rotation])
|
||||
|
||||
return <div ref={rContainer} {...rest} className={classNames('tl-positioned', rest.className)} />
|
||||
})
|
|
@ -2,9 +2,9 @@ import { useStateTracking, useValue } from '@tldraw/state'
|
|||
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import classNames from 'classnames'
|
||||
import * as React from 'react'
|
||||
import { useEditor } from '../..'
|
||||
import type { Editor } from '../editor/Editor'
|
||||
import { ShapeUtil } from '../editor/shapes/ShapeUtil'
|
||||
import { useEditor } from '../hooks/useEditor'
|
||||
import { useEditorComponents } from '../hooks/useEditorComponents'
|
||||
import { OptionalErrorBoundary } from './ErrorBoundary'
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ import {
|
|||
} from '../components/default-components/DefaultSnapIndictor'
|
||||
import { DefaultSpinner, TLSpinnerComponent } from '../components/default-components/DefaultSpinner'
|
||||
import { DefaultSvgDefs, TLSvgDefsComponent } from '../components/default-components/DefaultSvgDefs'
|
||||
import { useShallowObjectIdentity } from './useIdentity'
|
||||
|
||||
export interface BaseEditorComponents {
|
||||
Background: TLBackgroundComponent
|
||||
|
@ -97,7 +98,11 @@ type ComponentsContextProviderProps = {
|
|||
children: any
|
||||
}
|
||||
|
||||
export function EditorComponentsProvider({ overrides, children }: ComponentsContextProviderProps) {
|
||||
export function EditorComponentsProvider({
|
||||
overrides = {},
|
||||
children,
|
||||
}: ComponentsContextProviderProps) {
|
||||
const _overrides = useShallowObjectIdentity(overrides)
|
||||
return (
|
||||
<EditorComponentsContext.Provider
|
||||
value={useMemo(
|
||||
|
@ -127,9 +132,9 @@ export function EditorComponentsProvider({ overrides, children }: ComponentsCont
|
|||
HoveredShapeIndicator: DefaultHoveredShapeIndicator,
|
||||
OnTheCanvas: null,
|
||||
InFrontOfTheCanvas: null,
|
||||
...overrides,
|
||||
..._overrides,
|
||||
}),
|
||||
[overrides]
|
||||
[_overrides]
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -12,18 +12,7 @@ export const featureFlags: Record<string, DebugFlag<boolean>> = {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
export const debugFlags = {
|
||||
// --- DEBUG VALUES ---
|
||||
preventDefaultLogging: createDebugValue('preventDefaultLogging', {
|
||||
defaults: { all: false },
|
||||
}),
|
||||
pointerCaptureLogging: createDebugValue('pointerCaptureLogging', {
|
||||
defaults: { all: false },
|
||||
}),
|
||||
pointerCaptureTracking: createDebugValue('pointerCaptureTracking', {
|
||||
defaults: { all: false },
|
||||
}),
|
||||
pointerCaptureTrackingObject: createDebugValue(
|
||||
export const pointerCaptureTrackingObject = createDebugValue(
|
||||
'pointerCaptureTrackingObject',
|
||||
// ideally we wouldn't store this mutable value in an atom but it's not
|
||||
// a big deal for debug values
|
||||
|
@ -31,7 +20,17 @@ export const debugFlags = {
|
|||
defaults: { all: new Map<Element, number>() },
|
||||
shouldStoreForSession: false,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
/** @internal */
|
||||
export const debugFlags: Record<string, DebugFlag<boolean>> = {
|
||||
// --- DEBUG VALUES ---
|
||||
preventDefaultLogging: createDebugValue('preventDefaultLogging', {
|
||||
defaults: { all: false },
|
||||
}),
|
||||
pointerCaptureTracking: createDebugValue('pointerCaptureTracking', {
|
||||
defaults: { all: false },
|
||||
}),
|
||||
elementRemovalLogging: createDebugValue('elementRemovalLogging', {
|
||||
defaults: { all: false },
|
||||
}),
|
||||
|
@ -44,7 +43,6 @@ export const debugFlags = {
|
|||
throwToBlob: createDebugValue('throwToBlob', {
|
||||
defaults: { all: false },
|
||||
}),
|
||||
logMessages: createDebugValue('uiLog', { defaults: { all: [] as any[] } }),
|
||||
resetConnectionEveryPing: createDebugValue('resetConnectionEveryPing', {
|
||||
defaults: { all: false },
|
||||
}),
|
||||
|
@ -62,12 +60,6 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.tldrawLog = (message: any) => {
|
||||
debugFlags.logMessages.set(debugFlags.logMessages.get().concat(message))
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. USE ---
|
||||
// In normal code, read from debug flags directly by calling .value on them:
|
||||
// if (debugFlags.preventDefaultLogging.value) { ... }
|
||||
|
|
|
@ -14,7 +14,7 @@ whatever reason.
|
|||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { debugFlags } from './debug-flags'
|
||||
import { debugFlags, pointerCaptureTrackingObject } from './debug-flags'
|
||||
|
||||
/** @public */
|
||||
export function loopToHtmlElement(elm: Element): HTMLElement {
|
||||
|
@ -49,10 +49,8 @@ export function setPointerCapture(
|
|||
) {
|
||||
element.setPointerCapture(event.pointerId)
|
||||
if (debugFlags.pointerCaptureTracking.get()) {
|
||||
const trackingObj = debugFlags.pointerCaptureTrackingObject.get()
|
||||
const trackingObj = pointerCaptureTrackingObject.get()
|
||||
trackingObj.set(element, (trackingObj.get(element) ?? 0) + 1)
|
||||
}
|
||||
if (debugFlags.pointerCaptureLogging.get()) {
|
||||
console.warn('setPointerCapture called on element:', element, event)
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +66,7 @@ export function releasePointerCapture(
|
|||
|
||||
element.releasePointerCapture(event.pointerId)
|
||||
if (debugFlags.pointerCaptureTracking.get()) {
|
||||
const trackingObj = debugFlags.pointerCaptureTrackingObject.get()
|
||||
const trackingObj = pointerCaptureTrackingObject.get()
|
||||
if (trackingObj.get(element) === 1) {
|
||||
trackingObj.delete(element)
|
||||
} else if (trackingObj.has(element)) {
|
||||
|
@ -77,10 +75,8 @@ export function releasePointerCapture(
|
|||
console.warn('Release without capture')
|
||||
}
|
||||
}
|
||||
if (debugFlags.pointerCaptureLogging.get()) {
|
||||
console.warn('releasePointerCapture called on element:', element, event)
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const stopEventPropagation = (e: any) => e.stopPropagation()
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
@ -39,56 +39,29 @@ export { SelectTool } from './lib/tools/SelectTool/SelectTool'
|
|||
export { ZoomTool } from './lib/tools/ZoomTool/ZoomTool'
|
||||
// UI
|
||||
export { TldrawUi, type TldrawUiBaseProps, type TldrawUiProps } from './lib/ui/TldrawUi'
|
||||
export {
|
||||
TldrawUiContextProvider,
|
||||
type TldrawUiContextProviderProps,
|
||||
} from './lib/ui/TldrawUiContextProvider'
|
||||
export { setDefaultUiAssetUrls, type TLUiAssetUrlOverrides } from './lib/ui/assetUrls'
|
||||
export { ContextMenu, type TLUiContextMenuProps } from './lib/ui/components/ContextMenu'
|
||||
export { OfflineIndicator } from './lib/ui/components/OfflineIndicator/OfflineIndicator'
|
||||
export { Spinner } from './lib/ui/components/Spinner'
|
||||
export { Button, type TLUiButtonProps } from './lib/ui/components/primitives/Button'
|
||||
export { Icon, type TLUiIconProps } from './lib/ui/components/primitives/Icon'
|
||||
export { Input, type TLUiInputProps } from './lib/ui/components/primitives/Input'
|
||||
export {
|
||||
compactMenuItems,
|
||||
findMenuItem,
|
||||
menuCustom,
|
||||
menuGroup,
|
||||
menuItem,
|
||||
menuSubmenu,
|
||||
type TLUiCustomMenuItem,
|
||||
type TLUiMenuChild,
|
||||
type TLUiMenuGroup,
|
||||
type TLUiMenuItem,
|
||||
type TLUiMenuSchema,
|
||||
type TLUiSubMenu,
|
||||
} from './lib/ui/hooks/menuHelpers'
|
||||
TldrawUiContextProvider,
|
||||
type TldrawUiContextProviderProps,
|
||||
} from './lib/ui/context/TldrawUiContextProvider'
|
||||
export {
|
||||
useActions,
|
||||
type TLUiActionItem,
|
||||
type TLUiActionsContextType,
|
||||
} from './lib/ui/hooks/useActions'
|
||||
export {
|
||||
useActionsMenuSchema,
|
||||
type TLUiActionsMenuSchemaContextType,
|
||||
} from './lib/ui/hooks/useActionsMenuSchema'
|
||||
export { AssetUrlsProvider, useAssetUrls } from './lib/ui/hooks/useAssetUrls'
|
||||
export { BreakPointProvider, useBreakpoint } from './lib/ui/hooks/useBreakpoint'
|
||||
export { useCanRedo } from './lib/ui/hooks/useCanRedo'
|
||||
export { useCanUndo } from './lib/ui/hooks/useCanUndo'
|
||||
export { useMenuClipboardEvents, useNativeClipboardEvents } from './lib/ui/hooks/useClipboardEvents'
|
||||
export {
|
||||
useContextMenuSchema,
|
||||
type TLUiContextTTLUiMenuSchemaContextType,
|
||||
} from './lib/ui/hooks/useContextMenuSchema'
|
||||
export { useCopyAs } from './lib/ui/hooks/useCopyAs'
|
||||
} from './lib/ui/context/actions'
|
||||
export { AssetUrlsProvider, useAssetUrls } from './lib/ui/context/asset-urls'
|
||||
export { BreakPointProvider, useBreakpoint } from './lib/ui/context/breakpoints'
|
||||
export {
|
||||
useDialogs,
|
||||
type TLUiDialog,
|
||||
type TLUiDialogProps,
|
||||
type TLUiDialogsContextType,
|
||||
} from './lib/ui/hooks/useDialogsProvider'
|
||||
} from './lib/ui/context/dialogs'
|
||||
export {
|
||||
UiEventsProvider,
|
||||
useUiEvents,
|
||||
|
@ -97,32 +70,20 @@ export {
|
|||
type TLUiEventHandler,
|
||||
type TLUiEventMap,
|
||||
type TLUiEventSource,
|
||||
} from './lib/ui/hooks/useEventsProvider'
|
||||
export { useExportAs } from './lib/ui/hooks/useExportAs'
|
||||
export {
|
||||
useHelpMenuSchema,
|
||||
type TLUiHelpMenuSchemaContextType,
|
||||
} from './lib/ui/hooks/useHelpMenuSchema'
|
||||
export { useKeyboardShortcuts } from './lib/ui/hooks/useKeyboardShortcuts'
|
||||
export {
|
||||
useKeyboardShortcutsSchema,
|
||||
type TLUiKeyboardShortcutsSchemaContextType,
|
||||
type TLUiKeyboardShortcutsSchemaProviderProps,
|
||||
} from './lib/ui/hooks/useKeyboardShortcutsSchema'
|
||||
export { useLocalStorageState } from './lib/ui/hooks/useLocalStorageState'
|
||||
export { useMenuIsOpen } from './lib/ui/hooks/useMenuIsOpen'
|
||||
export {
|
||||
useMenuSchema,
|
||||
type TLUiMenuSchemaContextType,
|
||||
type TLUiMenuSchemaProviderProps,
|
||||
} from './lib/ui/hooks/useMenuSchema'
|
||||
export { useReadonly } from './lib/ui/hooks/useReadonly'
|
||||
} from './lib/ui/context/events'
|
||||
export {
|
||||
useToasts,
|
||||
type TLUiToast,
|
||||
type TLUiToastAction,
|
||||
type TLUiToastsContextType,
|
||||
} from './lib/ui/hooks/useToastsProvider'
|
||||
} from './lib/ui/context/toasts'
|
||||
export { useMenuClipboardEvents, useNativeClipboardEvents } from './lib/ui/hooks/useClipboardEvents'
|
||||
export { useCopyAs } from './lib/ui/hooks/useCopyAs'
|
||||
export { useExportAs } from './lib/ui/hooks/useExportAs'
|
||||
export { useKeyboardShortcuts } from './lib/ui/hooks/useKeyboardShortcuts'
|
||||
export { useLocalStorageState } from './lib/ui/hooks/useLocalStorageState'
|
||||
export { useMenuIsOpen } from './lib/ui/hooks/useMenuIsOpen'
|
||||
export { useReadonly } from './lib/ui/hooks/useReadonly'
|
||||
export {
|
||||
toolbarItem,
|
||||
useToolbarSchema,
|
||||
|
@ -170,9 +131,150 @@ export {
|
|||
type TldrawFile,
|
||||
} from './lib/utils/tldr/file'
|
||||
|
||||
import * as Dialog from './lib/ui/components/primitives/Dialog'
|
||||
import * as DropdownMenu from './lib/ui/components/primitives/DropdownMenu'
|
||||
// Minimap default component
|
||||
export { DefaultMinimap } from './lib/ui/components/Minimap/DefaultMinimap'
|
||||
|
||||
// N.B. Preserve order of import / export here with this comment.
|
||||
// Sometimes this can cause an import problem depending on build setup downstream.
|
||||
export { Dialog, DropdownMenu }
|
||||
// Helper to unwrap label from action items
|
||||
export { unwrapLabel } from './lib/ui/context/actions'
|
||||
|
||||
// General UI components for building menus
|
||||
export {
|
||||
TldrawUiMenuCheckboxItem,
|
||||
type TLUiMenuCheckboxItemProps,
|
||||
} from './lib/ui/components/menus/TldrawUiMenuCheckboxItem'
|
||||
export {
|
||||
TldrawUiMenuContextProvider,
|
||||
type TLUiMenuContextProviderProps,
|
||||
} from './lib/ui/components/menus/TldrawUiMenuContext'
|
||||
export {
|
||||
TldrawUiMenuGroup,
|
||||
type TLUiMenuGroupProps,
|
||||
} from './lib/ui/components/menus/TldrawUiMenuGroup'
|
||||
export {
|
||||
TldrawUiMenuItem,
|
||||
type TLUiMenuItemProps,
|
||||
} from './lib/ui/components/menus/TldrawUiMenuItem'
|
||||
export {
|
||||
TldrawUiMenuSubmenu,
|
||||
type TLUiMenuSubmenuProps,
|
||||
} from './lib/ui/components/menus/TldrawUiMenuSubmenu'
|
||||
|
||||
export {
|
||||
TldrawUiComponentsProvider,
|
||||
useTldrawUiComponents,
|
||||
type TLUiComponents,
|
||||
} from './lib/ui/context/components'
|
||||
|
||||
// Menus / UI elements that can be customized
|
||||
export { DefaultPageMenu } from './lib/ui/components/PageMenu/DefaultPageMenu'
|
||||
|
||||
export {
|
||||
DefaultStylePanel,
|
||||
type TLUiStylePanelProps,
|
||||
} from './lib/ui/components/StylePanel/DefaultStylePanel'
|
||||
export {
|
||||
DefaultStylePanelContent,
|
||||
type TLUiStylePanelContentProps,
|
||||
} from './lib/ui/components/StylePanel/DefaultStylePanelContent'
|
||||
|
||||
export {
|
||||
DefaultActionsMenu,
|
||||
type TLUiActionsMenuProps,
|
||||
} from './lib/ui/components/ActionsMenu/DefaultActionsMenu'
|
||||
export { DefaultActionsMenuContent } from './lib/ui/components/ActionsMenu/DefaultActionsMenuContent'
|
||||
|
||||
export {
|
||||
DefaultContextMenu as ContextMenu,
|
||||
DefaultContextMenu,
|
||||
type TLUiContextMenuProps,
|
||||
} from './lib/ui/components/ContextMenu/DefaultContextMenu'
|
||||
export { DefaultContextMenuContent } from './lib/ui/components/ContextMenu/DefaultContextMenuContent'
|
||||
|
||||
export {
|
||||
DefaultHelpMenu,
|
||||
type TLUiHelpMenuProps,
|
||||
} from './lib/ui/components/HelpMenu/DefaultHelpMenu'
|
||||
export { DefaultHelpMenuContent } from './lib/ui/components/HelpMenu/DefaultHelpMenuContent'
|
||||
|
||||
export {
|
||||
DefaultMainMenu,
|
||||
type TLUiMainMenuProps,
|
||||
} from './lib/ui/components/MainMenu/DefaultMainMenu'
|
||||
export { DefaultMainMenuContent } from './lib/ui/components/MainMenu/DefaultMainMenuContent'
|
||||
|
||||
export {
|
||||
DefaultQuickActions,
|
||||
type TLUiQuickActionsProps,
|
||||
} from './lib/ui/components/QuickActions/DefaultQuickActions'
|
||||
export { DefaultQuickActionsContent } from './lib/ui/components/QuickActions/DefaultQuickActionsContent'
|
||||
|
||||
export {
|
||||
DefaultZoomMenu,
|
||||
type TLUiZoomMenuProps,
|
||||
} from './lib/ui/components/ZoomMenu/DefaultZoomMenu'
|
||||
export { DefaultZoomMenuContent } from './lib/ui/components/ZoomMenu/DefaultZoomMenuContent'
|
||||
|
||||
export {
|
||||
DefaultHelperButtons,
|
||||
type TLUiHelperButtonsProps,
|
||||
} from './lib/ui/components/HelperButtons/DefaultHelperButtons'
|
||||
export { DefaultHelperButtonsContent } from './lib/ui/components/HelperButtons/DefaultHelperButtonsContent'
|
||||
|
||||
export {
|
||||
DefaultKeyboardShortcutsDialog,
|
||||
type TLUiKeyboardShortcutsDialogProps,
|
||||
} from './lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialog'
|
||||
export { DefaultKeyboardShortcutsDialogContent } from './lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent'
|
||||
|
||||
export {
|
||||
DefaultDebugMenu,
|
||||
type TLUiDebugMenuProps,
|
||||
} from './lib/ui/components/DebugMenu/DefaultDebugMenu'
|
||||
export { DefaultDebugMenuContent } from './lib/ui/components/DebugMenu/DefaultDebugMenuContent'
|
||||
|
||||
export { DefaultToolbar } from './lib/ui/components/Toolbar/DefaultToolbar'
|
||||
|
||||
export { type TLComponents } from './lib/Tldraw'
|
||||
|
||||
export {
|
||||
DialogBody,
|
||||
DialogCloseButton,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
type TLUiDialogBodyProps,
|
||||
type TLUiDialogFooterProps,
|
||||
type TLUiDialogHeaderProps,
|
||||
type TLUiDialogTitleProps,
|
||||
} from './lib/ui/components/primitives/Dialog'
|
||||
|
||||
export {
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuIndicator,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuRoot,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
type TLUiDropdownMenuCheckboxItemProps,
|
||||
type TLUiDropdownMenuContentProps,
|
||||
type TLUiDropdownMenuGroupProps,
|
||||
type TLUiDropdownMenuItemProps,
|
||||
type TLUiDropdownMenuRadioItemProps,
|
||||
type TLUiDropdownMenuRootProps,
|
||||
type TLUiDropdownMenuSubProps,
|
||||
type TLUiDropdownMenuSubTriggerProps,
|
||||
type TLUiDropdownMenuTriggerProps,
|
||||
} from './lib/ui/components/primitives/DropdownMenu'
|
||||
|
||||
export {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
type TLUiPopoverContentProps,
|
||||
type TLUiPopoverProps,
|
||||
type TLUiPopoverTriggerProps,
|
||||
} from './lib/ui/components/primitives/Popover'
|
||||
|
|
|
@ -4,13 +4,13 @@ import {
|
|||
ErrorScreen,
|
||||
LoadingScreen,
|
||||
StoreSnapshot,
|
||||
TLEditorComponents,
|
||||
TLOnMountHandler,
|
||||
TLRecord,
|
||||
TLStore,
|
||||
TLStoreWithStatus,
|
||||
TldrawEditor,
|
||||
TldrawEditorBaseProps,
|
||||
TldrawEditorProps,
|
||||
assert,
|
||||
useEditor,
|
||||
useShallowArrayIdentity,
|
||||
|
@ -31,14 +31,24 @@ import { defaultShapeUtils } from './defaultShapeUtils'
|
|||
import { registerDefaultSideEffects } from './defaultSideEffects'
|
||||
import { defaultTools } from './defaultTools'
|
||||
import { TldrawUi, TldrawUiProps } from './ui/TldrawUi'
|
||||
import { ContextMenu } from './ui/components/ContextMenu'
|
||||
import { TLUiComponents, useTldrawUiComponents } from './ui/context/components'
|
||||
import { usePreloadAssets } from './ui/hooks/usePreloadAssets'
|
||||
import { useDefaultEditorAssetsWithOverrides } from './utils/static-assets/assetUrls'
|
||||
|
||||
/**@public */
|
||||
export type TldrawProps = TldrawEditorBaseProps &
|
||||
(
|
||||
| {
|
||||
export type TLComponents = TLEditorComponents & TLUiComponents
|
||||
|
||||
/** @public */
|
||||
export type TldrawProps =
|
||||
// combine components from base editor and ui
|
||||
(Omit<TldrawUiProps, 'components'> &
|
||||
Omit<TldrawEditorBaseProps, 'components'> & {
|
||||
components?: TLComponents
|
||||
}) &
|
||||
// external content
|
||||
Partial<TLExternalContentProps> &
|
||||
// store stuff
|
||||
(| {
|
||||
store: TLStore | TLStoreWithStatus
|
||||
}
|
||||
| {
|
||||
|
@ -51,9 +61,7 @@ export type TldrawProps = TldrawEditorBaseProps &
|
|||
*/
|
||||
snapshot?: StoreSnapshot<TLRecord>
|
||||
}
|
||||
) &
|
||||
TldrawUiProps &
|
||||
Partial<TLExternalContentProps>
|
||||
)
|
||||
|
||||
/** @public */
|
||||
export function Tldraw(props: TldrawProps) {
|
||||
|
@ -64,17 +72,14 @@ export function Tldraw(props: TldrawProps) {
|
|||
acceptedImageMimeTypes,
|
||||
acceptedVideoMimeTypes,
|
||||
onMount,
|
||||
components = {},
|
||||
shapeUtils = [],
|
||||
tools = [],
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const components = useShallowObjectIdentity(rest.components ?? {})
|
||||
const shapeUtils = useShallowArrayIdentity(rest.shapeUtils ?? [])
|
||||
const tools = useShallowArrayIdentity(rest.tools ?? [])
|
||||
|
||||
const withDefaults: TldrawEditorProps = {
|
||||
initialState: 'select',
|
||||
...rest,
|
||||
components: useMemo(
|
||||
const _components = useShallowObjectIdentity(components)
|
||||
const componentsWithDefault = useMemo(
|
||||
() => ({
|
||||
Scribble: TldrawScribble,
|
||||
CollaboratorScribble: TldrawScribble,
|
||||
|
@ -82,13 +87,22 @@ export function Tldraw(props: TldrawProps) {
|
|||
SelectionBackground: TldrawSelectionBackground,
|
||||
Handles: TldrawHandles,
|
||||
HoveredShapeIndicator: TldrawHoveredShapeIndicator,
|
||||
...components,
|
||||
..._components,
|
||||
}),
|
||||
[components]
|
||||
),
|
||||
shapeUtils: useMemo(() => [...defaultShapeUtils, ...shapeUtils], [shapeUtils]),
|
||||
tools: useMemo(() => [...defaultTools, ...defaultShapeTools, ...tools], [tools]),
|
||||
}
|
||||
[_components]
|
||||
)
|
||||
|
||||
const _shapeUtils = useShallowArrayIdentity(shapeUtils)
|
||||
const shapeUtilsWithDefaults = useMemo(
|
||||
() => [...defaultShapeUtils, ..._shapeUtils],
|
||||
[_shapeUtils]
|
||||
)
|
||||
|
||||
const _tools = useShallowArrayIdentity(tools)
|
||||
const toolsWithDefaults = useMemo(
|
||||
() => [...defaultTools, ...defaultShapeTools, ..._tools],
|
||||
[_tools]
|
||||
)
|
||||
|
||||
const assets = useDefaultEditorAssetsWithOverrides(rest.assetUrls)
|
||||
|
||||
|
@ -103,11 +117,14 @@ export function Tldraw(props: TldrawProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<TldrawEditor {...withDefaults}>
|
||||
<TldrawUi {...withDefaults}>
|
||||
<ContextMenu>
|
||||
<Canvas />
|
||||
</ContextMenu>
|
||||
<TldrawEditor
|
||||
initialState="select"
|
||||
{...rest}
|
||||
components={componentsWithDefault}
|
||||
shapeUtils={shapeUtilsWithDefaults}
|
||||
tools={toolsWithDefaults}
|
||||
>
|
||||
<TldrawUi {...rest} components={componentsWithDefault}>
|
||||
<InsideOfEditorContext
|
||||
maxImageDimension={maxImageDimension}
|
||||
maxAssetSize={maxAssetSize}
|
||||
|
@ -121,12 +138,21 @@ export function Tldraw(props: TldrawProps) {
|
|||
)
|
||||
}
|
||||
|
||||
const defaultAcceptedImageMimeTypes = Object.freeze([
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/svg+xml',
|
||||
])
|
||||
|
||||
const defaultAcceptedVideoMimeTypes = Object.freeze(['video/mp4', 'video/quicktime'])
|
||||
|
||||
// We put these hooks into a component here so that they can run inside of the context provided by TldrawEditor.
|
||||
function InsideOfEditorContext({
|
||||
maxImageDimension = 1000,
|
||||
maxAssetSize = 10 * 1024 * 1024, // 10mb
|
||||
acceptedImageMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'],
|
||||
acceptedVideoMimeTypes = ['video/mp4', 'video/quicktime'],
|
||||
acceptedImageMimeTypes = defaultAcceptedImageMimeTypes,
|
||||
acceptedVideoMimeTypes = defaultAcceptedVideoMimeTypes,
|
||||
onMount,
|
||||
}: Partial<TLExternalContentProps & { onMount: TLOnMountHandler }>) {
|
||||
const editor = useEditor()
|
||||
|
@ -156,7 +182,10 @@ function InsideOfEditorContext({
|
|||
if (editor) return onMountEvent?.(editor)
|
||||
}, [editor, onMountEvent])
|
||||
|
||||
return null
|
||||
const { ContextMenu } = useTldrawUiComponents()
|
||||
if (!ContextMenu) return <Canvas />
|
||||
|
||||
return <ContextMenu canvas={<Canvas />} />
|
||||
}
|
||||
|
||||
// duped from tldraw editor
|
||||
|
|
|
@ -30,9 +30,9 @@ export type TLExternalContentProps = {
|
|||
// The maximum size (in bytes) of an asset. Assets larger than this will be rejected. Defaults to 10mb (10 * 1024 * 1024).
|
||||
maxAssetSize: number
|
||||
// The mime types of images that are allowed to be handled. Defaults to ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'].
|
||||
acceptedImageMimeTypes: string[]
|
||||
acceptedImageMimeTypes: readonly string[]
|
||||
// The mime types of videos that are allowed to be handled. Defaults to ['video/mp4', 'video/webm', 'video/quicktime'].
|
||||
acceptedVideoMimeTypes: string[]
|
||||
acceptedVideoMimeTypes: readonly string[]
|
||||
}
|
||||
|
||||
export function registerDefaultExternalContentHandlers(
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
.tlui-button:disabled {
|
||||
color: var(--color-text-3);
|
||||
text-shadow: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tlui-button:disabled .tlui-kbd {
|
||||
|
@ -310,6 +311,9 @@
|
|||
.tlui-buttons__horizontal > *:nth-last-child(1) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
.tlui-buttons__horizontal > *:only-child {
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
/* Button Grid */
|
||||
|
||||
|
@ -504,7 +508,6 @@
|
|||
grid-auto-flow: column;
|
||||
grid-template-columns: auto;
|
||||
grid-auto-columns: minmax(1em, auto);
|
||||
gap: 1px;
|
||||
align-self: bottom;
|
||||
color: var(--color-text-1);
|
||||
margin-left: var(--space-4);
|
||||
|
@ -860,6 +863,10 @@
|
|||
height: 48px;
|
||||
}
|
||||
|
||||
.tlui-toolbar__extras:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tlui-toolbar__extras__controls {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
@ -928,6 +935,10 @@
|
|||
|
||||
/* ---------------------- Menu ---------------------- */
|
||||
|
||||
.tlui-menu:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tlui-menu {
|
||||
z-index: var(--layer-menus);
|
||||
height: fit-content;
|
||||
|
@ -954,24 +965,15 @@
|
|||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.tlui-menu__group[data-size='large'] {
|
||||
min-width: initial;
|
||||
.tlui-menu__group:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tlui-menu__group[data-size='medium'] {
|
||||
min-width: 144px;
|
||||
.tlui-menu__group {
|
||||
border-bottom: 1px solid var(--color-divider);
|
||||
}
|
||||
|
||||
.tlui-menu__group[data-size='small'] {
|
||||
min-width: 96px;
|
||||
}
|
||||
|
||||
.tlui-menu__group[data-size='tiny'] {
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
.tlui-menu__group + .tlui-menu__group {
|
||||
border-top: 1px solid var(--color-divider);
|
||||
.tlui-menu__group:nth-last-of-type(1) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tlui-menu__submenu__trigger[data-state='open']:not(:hover)::after {
|
||||
|
@ -984,6 +986,27 @@
|
|||
background: linear-gradient(270deg, rgba(144, 144, 144, 0) 0%, var(--color-muted-2) 100%);
|
||||
}
|
||||
|
||||
/* Menu Sizes */
|
||||
|
||||
.tlui-menu[data-size='large'] > .tlui-menu__group,
|
||||
.tlui-menu__submenu__content[data-size='large'] > .tlui-menu__group {
|
||||
min-width: initial;
|
||||
}
|
||||
|
||||
.tlui-menu[data-size='medium'] > .tlui-menu__group,
|
||||
.tlui-menu__submenu__content[data-size='medium'] > .tlui-menu__group {
|
||||
min-width: 144px;
|
||||
}
|
||||
|
||||
.tlui-menu[data-size='small'] > .tlui-menu__group,
|
||||
.tlui-menu__submenu__content[data-size='small'] > .tlui-menu__group {
|
||||
min-width: 96px;
|
||||
}
|
||||
|
||||
.tlui-menu[data-size='tiny'] > .tlui-menu__group,
|
||||
.tlui-menu__submenu__content[data-size='tiny'] > .tlui-menu__group {
|
||||
min-width: 0px;
|
||||
}
|
||||
/* ------------------ Actions Menu ------------------ */
|
||||
|
||||
.tlui-actions-menu {
|
||||
|
@ -1105,7 +1128,7 @@
|
|||
|
||||
/* ------------------- Navigation ------------------- */
|
||||
|
||||
.tlui-navigation-zone {
|
||||
.tlui-navigation-panel {
|
||||
display: flex;
|
||||
width: min-content;
|
||||
flex-direction: column;
|
||||
|
@ -1116,7 +1139,7 @@
|
|||
bottom: 0px;
|
||||
}
|
||||
|
||||
.tlui-navigation-zone::before {
|
||||
.tlui-navigation-panel::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
@ -1129,16 +1152,16 @@
|
|||
background-color: var(--color-low);
|
||||
}
|
||||
|
||||
.tlui-navigation-zone__toggle .tlui-icon {
|
||||
.tlui-navigation-panel__toggle .tlui-icon {
|
||||
opacity: 0.24;
|
||||
}
|
||||
|
||||
.tlui-navigation-zone__toggle:active .tlui-icon {
|
||||
.tlui-navigation-panel__toggle:active .tlui-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.tlui-navigation-zone__toggle:hover .tlui-icon {
|
||||
.tlui-navigation-panel__toggle:hover .tlui-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,26 +2,24 @@ import { ToastProvider } from '@radix-ui/react-toast'
|
|||
import { useEditor, useValue } from '@tldraw/editor'
|
||||
import classNames from 'classnames'
|
||||
import React, { ReactNode } from 'react'
|
||||
import { TldrawUiContextProvider, TldrawUiContextProviderProps } from './TldrawUiContextProvider'
|
||||
import { TLUiAssetUrlOverrides } from './assetUrls'
|
||||
import { BackToContent } from './components/BackToContent'
|
||||
import { DebugPanel } from './components/DebugPanel'
|
||||
import { Dialogs } from './components/Dialogs'
|
||||
import { FollowingIndicator } from './components/FollowingIndicator'
|
||||
import { HelpMenu } from './components/HelpMenu'
|
||||
import { MenuZone } from './components/MenuZone'
|
||||
import { NavigationZone } from './components/NavigationZone/NavigationZone'
|
||||
import { ExitPenMode } from './components/PenModeToggle'
|
||||
import { StopFollowing } from './components/StopFollowing'
|
||||
import { StylePanel } from './components/StylePanel/StylePanel'
|
||||
import { ToastViewport, Toasts } from './components/Toasts'
|
||||
import { Toolbar } from './components/Toolbar/Toolbar'
|
||||
import { Button } from './components/primitives/Button'
|
||||
import { useActions } from './hooks/useActions'
|
||||
import { useBreakpoint } from './hooks/useBreakpoint'
|
||||
import {
|
||||
TldrawUiContextProvider,
|
||||
TldrawUiContextProviderProps,
|
||||
} from './context/TldrawUiContextProvider'
|
||||
import { useActions } from './context/actions'
|
||||
import { useBreakpoint } from './context/breakpoints'
|
||||
import { TLUiComponents, useTldrawUiComponents } from './context/components'
|
||||
import { useNativeClipboardEvents } from './hooks/useClipboardEvents'
|
||||
import { useEditorEvents } from './hooks/useEditorEvents'
|
||||
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
|
||||
import { useRelevantStyles } from './hooks/useRevelantStyles'
|
||||
import { useTranslation } from './hooks/useTranslation/useTranslation'
|
||||
|
||||
/**
|
||||
|
@ -47,6 +45,11 @@ export interface TldrawUiBaseProps {
|
|||
*/
|
||||
hideUi?: boolean
|
||||
|
||||
/**
|
||||
* Overrides for the UI components.
|
||||
*/
|
||||
components?: TLUiComponents
|
||||
|
||||
/**
|
||||
* A component to use for the share zone (will be deprecated)
|
||||
*/
|
||||
|
@ -76,10 +79,11 @@ export const TldrawUi = React.memo(function TldrawUi({
|
|||
renderDebugMenuItems,
|
||||
children,
|
||||
hideUi,
|
||||
components,
|
||||
...rest
|
||||
}: TldrawUiProps) {
|
||||
return (
|
||||
<TldrawUiContextProvider {...rest}>
|
||||
<TldrawUiContextProvider {...rest} components={components}>
|
||||
<TldrawUiInner
|
||||
hideUi={hideUi}
|
||||
shareZone={shareZone}
|
||||
|
@ -116,11 +120,7 @@ const TldrawUiInner = React.memo(function TldrawUiInner({
|
|||
)
|
||||
})
|
||||
|
||||
const TldrawUiContent = React.memo(function TldrawUI({
|
||||
shareZone,
|
||||
topZone,
|
||||
renderDebugMenuItems,
|
||||
}: TldrawUiContentProps) {
|
||||
const TldrawUiContent = React.memo(function TldrawUI({ shareZone, topZone }: TldrawUiContentProps) {
|
||||
const editor = useEditor()
|
||||
const msg = useTranslation()
|
||||
const breakpoint = useBreakpoint()
|
||||
|
@ -130,6 +130,8 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
|||
const isFocusMode = useValue('focus', () => editor.getInstanceState().isFocusMode, [editor])
|
||||
const isDebugMode = useValue('debug', () => editor.getInstanceState().isDebugMode, [editor])
|
||||
|
||||
const { StylePanel, Toolbar, HelpMenu, NavigationPanel, HelperButtons } = useTldrawUiComponents()
|
||||
|
||||
useKeyboardShortcuts()
|
||||
useNativeClipboardEvents()
|
||||
useEditorEvents()
|
||||
|
@ -159,29 +161,21 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
|||
<div className="tlui-layout__top">
|
||||
<div className="tlui-layout__top__left">
|
||||
<MenuZone />
|
||||
<div className="tlui-helper-buttons">
|
||||
<ExitPenMode />
|
||||
<BackToContent />
|
||||
<StopFollowing />
|
||||
</div>
|
||||
{HelperButtons && <HelperButtons />}
|
||||
</div>
|
||||
<div className="tlui-layout__top__center">{topZone}</div>
|
||||
<div className="tlui-layout__top__right">
|
||||
{shareZone}
|
||||
{breakpoint >= 5 && !isReadonlyMode && (
|
||||
<div className="tlui-style-panel__wrapper">
|
||||
<StylePanel />
|
||||
</div>
|
||||
)}
|
||||
{StylePanel && breakpoint >= 5 && !isReadonlyMode && <_StylePanel />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="tlui-layout__bottom">
|
||||
<div className="tlui-layout__bottom__main">
|
||||
<NavigationZone />
|
||||
<Toolbar />
|
||||
{breakpoint >= 4 && <HelpMenu />}
|
||||
{NavigationPanel && <NavigationPanel />}
|
||||
{Toolbar && <Toolbar />}
|
||||
{HelpMenu && <HelpMenu />}
|
||||
</div>
|
||||
{isDebugMode && <DebugPanel renderDebugMenuItems={renderDebugMenuItems ?? null} />}
|
||||
{isDebugMode && <DebugPanel />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
@ -193,3 +187,11 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
|||
</ToastProvider>
|
||||
)
|
||||
})
|
||||
|
||||
function _StylePanel() {
|
||||
const { StylePanel } = useTldrawUiComponents()
|
||||
const relevantStyles = useRelevantStyles()
|
||||
|
||||
if (!StylePanel) return null
|
||||
return <StylePanel relevantStyles={relevantStyles} />
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
||||
import { useContainer } from '@tldraw/editor'
|
||||
import { memo } from 'react'
|
||||
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||
import { useActionsMenuSchema } from '../hooks/useActionsMenuSchema'
|
||||
import { useBreakpoint } from '../hooks/useBreakpoint'
|
||||
import { useReadonly } from '../hooks/useReadonly'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { Button } from './primitives/Button'
|
||||
import { Popover, PopoverTrigger } from './primitives/Popover'
|
||||
import { kbdStr } from './primitives/shared'
|
||||
|
||||
export const ActionsMenu = memo(function ActionsMenu() {
|
||||
const msg = useTranslation()
|
||||
const container = useContainer()
|
||||
const menuSchema = useActionsMenuSchema()
|
||||
const isReadonly = useReadonly()
|
||||
const breakpoint = useBreakpoint()
|
||||
|
||||
function getActionMenuItem(item: TLUiMenuChild) {
|
||||
if (!item) return null
|
||||
if (isReadonly && !item.readonlyOk) return null
|
||||
|
||||
switch (item.type) {
|
||||
case 'item': {
|
||||
const { id, icon, label, kbd, onSelect } = item.actionItem
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={id}
|
||||
data-testid={`menu-item.${item.id}`}
|
||||
icon={icon}
|
||||
type="icon"
|
||||
title={
|
||||
label
|
||||
? kbd
|
||||
? `${msg(label)} ${kbdStr(kbd)}`
|
||||
: `${msg(label)}`
|
||||
: kbd
|
||||
? `${kbdStr(kbd)}`
|
||||
: ''
|
||||
}
|
||||
onClick={() => onSelect('actions-menu')}
|
||||
disabled={item.disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover id="actions-menu">
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
className="tlui-menu__trigger"
|
||||
data-testid="main.action-menu"
|
||||
icon="dots-vertical"
|
||||
title={msg('actions-menu.title')}
|
||||
type="icon" // needs to be here because the trigger also passes down type="button"
|
||||
smallIcon
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverPrimitive.Portal container={container}>
|
||||
<PopoverPrimitive.Content
|
||||
className="tlui-popover__content"
|
||||
side={breakpoint >= 6 ? 'bottom' : 'top'}
|
||||
dir="ltr"
|
||||
sideOffset={6}
|
||||
>
|
||||
<div className="tlui-actions-menu tlui-buttons__grid">
|
||||
{menuSchema.map(getActionMenuItem)}
|
||||
</div>
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Portal>
|
||||
</Popover>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,57 @@
|
|||
import { useEditor, useValue } from '@tldraw/editor'
|
||||
import { memo } from 'react'
|
||||
import { useBreakpoint } from '../../context/breakpoints'
|
||||
import { useReadonly } from '../../hooks/useReadonly'
|
||||
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/Popover'
|
||||
import { DefaultActionsMenuContent } from './DefaultActionsMenuContent'
|
||||
|
||||
/** @public */
|
||||
export type TLUiActionsMenuProps = {
|
||||
children?: any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const DefaultActionsMenu = memo(function DefaultActionsMenu({
|
||||
children,
|
||||
}: TLUiActionsMenuProps) {
|
||||
const msg = useTranslation()
|
||||
const breakpoint = useBreakpoint()
|
||||
|
||||
const isReadonlyMode = useReadonly()
|
||||
|
||||
const editor = useEditor()
|
||||
const isInAcceptableReadonlyState = useValue(
|
||||
'should display quick actions when in readonly',
|
||||
() => editor.isInAny('hand', 'zoom'),
|
||||
[editor]
|
||||
)
|
||||
|
||||
// Get the actions menu content, either the default component or the user's
|
||||
// override. If there's no menu content, then the user has set it to null,
|
||||
// so skip rendering the menu.
|
||||
|
||||
const content = children ?? <DefaultActionsMenuContent />
|
||||
if (isReadonlyMode && !isInAcceptableReadonlyState) return
|
||||
|
||||
return (
|
||||
<Popover id="actions-menu">
|
||||
<PopoverTrigger
|
||||
className="tlui-menu__trigger"
|
||||
data-testid="main.action-menu"
|
||||
icon="dots-vertical"
|
||||
title={msg('actions-menu.title')}
|
||||
type="icon" // needs to be here because the trigger also passes down type="button"
|
||||
smallIcon
|
||||
/>
|
||||
<PopoverContent side={breakpoint >= 6 ? 'bottom' : 'top'} sideOffset={6}>
|
||||
<div className="tlui-actions-menu tlui-buttons__grid">
|
||||
<TldrawUiMenuContextProvider type="icons" sourceId="actions-menu">
|
||||
{content}
|
||||
</TldrawUiMenuContextProvider>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,135 @@
|
|||
import { useEditor, useValue } from '@tldraw/editor'
|
||||
import { useActions } from '../../context/actions'
|
||||
import { useBreakpoint } from '../../context/breakpoints'
|
||||
import {
|
||||
useAllowGroup,
|
||||
useAllowUngroup,
|
||||
useHasLinkShapeSelected,
|
||||
useThreeStackableItems,
|
||||
useUnlockedSelectedShapesCount,
|
||||
} from '../../hooks/menu-hooks'
|
||||
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem'
|
||||
|
||||
/** @public */
|
||||
export function DefaultActionsMenuContent() {
|
||||
return (
|
||||
<>
|
||||
<AlignMenuItems />
|
||||
<DistributeMenuItems />
|
||||
<StackMenuItems />
|
||||
<ReorderMenuItems />
|
||||
<ZoomOrRotateMenuItem />
|
||||
<RotateCWMenuItem />
|
||||
<EditLinkMenuItem />
|
||||
<GroupOrUngroupMenuItem />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function AlignMenuItems() {
|
||||
const actions = useActions()
|
||||
const twoSelected = useUnlockedSelectedShapesCount(2)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TldrawUiMenuItem {...actions['align-left']} disabled={!twoSelected} />
|
||||
<TldrawUiMenuItem {...actions['align-center-horizontal']} disabled={!twoSelected} />
|
||||
<TldrawUiMenuItem {...actions['align-right']} disabled={!twoSelected} />
|
||||
<TldrawUiMenuItem {...actions['stretch-horizontal']} disabled={!twoSelected} />
|
||||
<TldrawUiMenuItem {...actions['align-top']} disabled={!twoSelected} />
|
||||
<TldrawUiMenuItem {...actions['align-center-vertical']} disabled={!twoSelected} />
|
||||
<TldrawUiMenuItem {...actions['align-bottom']} disabled={!twoSelected} />
|
||||
<TldrawUiMenuItem {...actions['stretch-vertical']} disabled={!twoSelected} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DistributeMenuItems() {
|
||||
const actions = useActions()
|
||||
const threeSelected = useUnlockedSelectedShapesCount(3)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TldrawUiMenuItem {...actions['distribute-horizontal']} disabled={!threeSelected} />
|
||||
<TldrawUiMenuItem {...actions['distribute-vertical']} disabled={!threeSelected} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function StackMenuItems() {
|
||||
const actions = useActions()
|
||||
const threeStackableItems = useThreeStackableItems()
|
||||
|
||||
return (
|
||||
<>
|
||||
<TldrawUiMenuItem {...actions['stack-horizontal']} disabled={!threeStackableItems} />
|
||||
<TldrawUiMenuItem {...actions['stack-vertical']} disabled={!threeStackableItems} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ReorderMenuItems() {
|
||||
const actions = useActions()
|
||||
const oneSelected = useUnlockedSelectedShapesCount(1)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TldrawUiMenuItem {...actions['send-to-back']} disabled={!oneSelected} />
|
||||
<TldrawUiMenuItem {...actions['send-backward']} disabled={!oneSelected} />
|
||||
<TldrawUiMenuItem {...actions['bring-forward']} disabled={!oneSelected} />
|
||||
<TldrawUiMenuItem {...actions['bring-to-front']} disabled={!oneSelected} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ZoomOrRotateMenuItem() {
|
||||
const breakpoint = useBreakpoint()
|
||||
return breakpoint < 5 ? <ZoomTo100MenuItem /> : <RotateCCWMenuItem />
|
||||
}
|
||||
|
||||
function ZoomTo100MenuItem() {
|
||||
const actions = useActions()
|
||||
const editor = useEditor()
|
||||
const isZoomedTo100 = useValue('zoom is 1', () => editor.getZoomLevel() === 1, [editor])
|
||||
|
||||
return <TldrawUiMenuItem {...actions['zoom-to-100']} disabled={isZoomedTo100} />
|
||||
}
|
||||
|
||||
function RotateCCWMenuItem() {
|
||||
const actions = useActions()
|
||||
const oneSelected = useUnlockedSelectedShapesCount(1)
|
||||
|
||||
return <TldrawUiMenuItem {...actions['rotate-ccw']} disabled={!oneSelected} />
|
||||
}
|
||||
|
||||
function RotateCWMenuItem() {
|
||||
const actions = useActions()
|
||||
const oneSelected = useUnlockedSelectedShapesCount(1)
|
||||
|
||||
return <TldrawUiMenuItem {...actions['rotate-cw']} disabled={!oneSelected} />
|
||||
}
|
||||
|
||||
function EditLinkMenuItem() {
|
||||
const actions = useActions()
|
||||
const showEditLink = useHasLinkShapeSelected()
|
||||
|
||||
return <TldrawUiMenuItem {...actions['edit-link']} disabled={!showEditLink} />
|
||||
}
|
||||
|
||||
function GroupOrUngroupMenuItem() {
|
||||
const allowGroup = useAllowGroup()
|
||||
const allowUngroup = useAllowUngroup()
|
||||
return allowGroup ? <GroupMenuItem /> : allowUngroup ? <UngroupMenuItem /> : <GroupMenuItem />
|
||||
}
|
||||
|
||||
function GroupMenuItem() {
|
||||
const actions = useActions()
|
||||
const twoSelected = useUnlockedSelectedShapesCount(2)
|
||||
|
||||
return <TldrawUiMenuItem {...actions['group']} disabled={!twoSelected} />
|
||||
}
|
||||
|
||||
function UngroupMenuItem() {
|
||||
const actions = useActions()
|
||||
return <TldrawUiMenuItem {...actions['ungroup']} />
|
||||
}
|
|
@ -1,233 +0,0 @@
|
|||
import * as _ContextMenu from '@radix-ui/react-context-menu'
|
||||
import { Editor, preventDefault, useContainer, useEditor, useValue } from '@tldraw/editor'
|
||||
import classNames from 'classnames'
|
||||
import { forwardRef, useCallback, useState } from 'react'
|
||||
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||
import { useBreakpoint } from '../hooks/useBreakpoint'
|
||||
import { useContextMenuSchema } from '../hooks/useContextMenuSchema'
|
||||
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
|
||||
import { useReadonly } from '../hooks/useReadonly'
|
||||
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { TLUiIconType } from '../icon-types'
|
||||
import { MoveToPageMenu } from './MoveToPageMenu'
|
||||
import { Button } from './primitives/Button'
|
||||
import { Icon } from './primitives/Icon'
|
||||
import { Kbd } from './primitives/Kbd'
|
||||
|
||||
/** @public */
|
||||
export interface TLUiContextMenuProps {
|
||||
children: any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const ContextMenu = function ContextMenu({ children }: { children: any }) {
|
||||
const editor = useEditor()
|
||||
|
||||
const contextTLUiMenuSchema = useContextMenuSchema()
|
||||
|
||||
const cb = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
const onlySelectedShape = editor.getOnlySelectedShape()
|
||||
|
||||
if (onlySelectedShape && editor.isShapeOrAncestorLocked(onlySelectedShape)) {
|
||||
editor.setSelectedShapes([])
|
||||
}
|
||||
} else {
|
||||
// Weird route: selecting locked shapes on long press
|
||||
if (editor.getInstanceState().isCoarsePointer) {
|
||||
const selectedShapes = editor.getSelectedShapes()
|
||||
const {
|
||||
inputs: { currentPagePoint },
|
||||
} = editor
|
||||
|
||||
// get all of the shapes under the current pointer
|
||||
const shapesAtPoint = editor.getShapesAtPoint(currentPagePoint)
|
||||
|
||||
if (
|
||||
// if there are no selected shapes
|
||||
!editor.getSelectedShapes().length ||
|
||||
// OR if none of the shapes at the point include the selected shape
|
||||
!shapesAtPoint.some((s) => selectedShapes.includes(s))
|
||||
) {
|
||||
// then are there any locked shapes under the current pointer?
|
||||
const lockedShapes = shapesAtPoint.filter((s) => editor.isShapeOrAncestorLocked(s))
|
||||
|
||||
if (lockedShapes.length) {
|
||||
// nice, let's select them
|
||||
editor.select(...lockedShapes.map((s) => s.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
||||
const [isOpen, handleOpenChange] = useMenuIsOpen('context menu', cb)
|
||||
|
||||
// If every item in the menu is readonly, then we don't want to show the menu
|
||||
const isReadonly = useReadonly()
|
||||
|
||||
const noItemsToShow =
|
||||
contextTLUiMenuSchema.length === 0 ||
|
||||
(isReadonly && contextTLUiMenuSchema.every((item) => !item.readonlyOk))
|
||||
|
||||
// Todo: remove this dependency on the select tool; not sure how else to say "only show the context menu when we're using a tool that supports it"
|
||||
const selectToolActive = useValue(
|
||||
'isSelectToolActive',
|
||||
() => editor.getCurrentToolId() === 'select',
|
||||
[editor]
|
||||
)
|
||||
|
||||
const disabled = !selectToolActive || noItemsToShow
|
||||
|
||||
return (
|
||||
<_ContextMenu.Root dir="ltr" onOpenChange={handleOpenChange} modal={false}>
|
||||
<_ContextMenu.Trigger
|
||||
onContextMenu={disabled ? preventDefault : undefined}
|
||||
dir="ltr"
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</_ContextMenu.Trigger>
|
||||
{isOpen && <ContextMenuContent />}
|
||||
</_ContextMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const ContextMenuContent = forwardRef(function ContextMenuContent() {
|
||||
const editor = useEditor()
|
||||
const msg = useTranslation()
|
||||
const menuSchema = useContextMenuSchema()
|
||||
|
||||
const [_, handleSubOpenChange] = useMenuIsOpen('context menu sub')
|
||||
|
||||
const isReadonly = useReadonly()
|
||||
const breakpoint = useBreakpoint()
|
||||
const container = useContainer()
|
||||
|
||||
const [disableClicks, setDisableClicks] = useState(false)
|
||||
|
||||
function getContextMenuItem(
|
||||
editor: Editor,
|
||||
item: TLUiMenuChild,
|
||||
parent: TLUiMenuChild | null,
|
||||
depth: number
|
||||
) {
|
||||
if (!item) return null
|
||||
if (isReadonly && !item.readonlyOk) return null
|
||||
|
||||
switch (item.type) {
|
||||
case 'custom': {
|
||||
switch (item.id) {
|
||||
case 'MOVE_TO_PAGE_MENU': {
|
||||
return <MoveToPageMenu key={item.id} />
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'group': {
|
||||
return (
|
||||
<_ContextMenu.Group
|
||||
dir="ltr"
|
||||
className={classNames('tlui-menu__group', {
|
||||
'tlui-menu__group__small': parent?.type === 'submenu',
|
||||
})}
|
||||
data-testid={`menu-item.${item.id}`}
|
||||
key={item.id}
|
||||
>
|
||||
{item.children.map((child) => getContextMenuItem(editor, child, item, depth + 1))}
|
||||
</_ContextMenu.Group>
|
||||
)
|
||||
}
|
||||
case 'submenu': {
|
||||
return (
|
||||
<_ContextMenu.Sub key={item.id} onOpenChange={handleSubOpenChange}>
|
||||
<_ContextMenu.SubTrigger dir="ltr" disabled={item.disabled} asChild>
|
||||
<Button
|
||||
type="menu"
|
||||
label={item.label as TLUiTranslationKey}
|
||||
data-testid={`menu-item.${item.id}`}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
</_ContextMenu.SubTrigger>
|
||||
<_ContextMenu.Portal container={container}>
|
||||
<_ContextMenu.SubContent className="tlui-menu" sideOffset={-4} collisionPadding={4}>
|
||||
{item.children.map((child) => getContextMenuItem(editor, child, item, depth + 1))}
|
||||
</_ContextMenu.SubContent>
|
||||
</_ContextMenu.Portal>
|
||||
</_ContextMenu.Sub>
|
||||
)
|
||||
}
|
||||
case 'item': {
|
||||
if (isReadonly && !item.readonlyOk) return null
|
||||
|
||||
const { id, checkbox, contextMenuLabel, label, onSelect, kbd, icon } = item.actionItem
|
||||
const labelToUse = contextMenuLabel ?? label
|
||||
const labelStr = labelToUse ? msg(labelToUse as TLUiTranslationKey) : undefined
|
||||
|
||||
if (checkbox) {
|
||||
// Item is in a checkbox group
|
||||
return (
|
||||
<_ContextMenu.CheckboxItem
|
||||
key={id}
|
||||
className="tlui-button tlui-button__menu tlui-button__checkbox"
|
||||
dir="ltr"
|
||||
disabled={item.disabled}
|
||||
onSelect={(e) => {
|
||||
onSelect('context-menu')
|
||||
preventDefault(e)
|
||||
}}
|
||||
title={labelStr ? labelStr : undefined}
|
||||
checked={item.checked}
|
||||
>
|
||||
<Icon small icon={item.checked ? 'check' : 'checkbox-empty'} />
|
||||
{labelStr && (
|
||||
<span className="tlui-button__label" draggable={false}>
|
||||
{labelStr}
|
||||
</span>
|
||||
)}
|
||||
{kbd && <Kbd>{kbd}</Kbd>}
|
||||
</_ContextMenu.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<_ContextMenu.Item key={id} dir="ltr" asChild>
|
||||
<Button
|
||||
type="menu"
|
||||
data-testid={`menu-item.${id}`}
|
||||
kbd={kbd}
|
||||
label={labelToUse as TLUiTranslationKey}
|
||||
disabled={item.disabled}
|
||||
iconLeft={breakpoint < 3 && depth > 2 ? (icon as TLUiIconType) : undefined}
|
||||
onClick={() => {
|
||||
if (disableClicks) {
|
||||
setDisableClicks(false)
|
||||
} else {
|
||||
onSelect('context-menu')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</_ContextMenu.Item>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<_ContextMenu.Portal container={container}>
|
||||
<_ContextMenu.Content
|
||||
className="tlui-menu scrollable"
|
||||
data-testid="context-menu"
|
||||
alignOffset={-4}
|
||||
collisionPadding={4}
|
||||
onContextMenu={preventDefault}
|
||||
>
|
||||
{menuSchema.map((item) => getContextMenuItem(editor, item, null, 0))}
|
||||
</_ContextMenu.Content>
|
||||
</_ContextMenu.Portal>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,90 @@
|
|||
import * as _ContextMenu from '@radix-ui/react-context-menu'
|
||||
import { preventDefault, useContainer, useEditor } from '@tldraw/editor'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
|
||||
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext'
|
||||
import { DefaultContextMenuContent } from './DefaultContextMenuContent'
|
||||
|
||||
/** @public */
|
||||
export interface TLUiContextMenuProps {
|
||||
canvas: any
|
||||
children?: any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const DefaultContextMenu = memo(function DefaultContextMenu({
|
||||
canvas,
|
||||
children,
|
||||
}: TLUiContextMenuProps) {
|
||||
const editor = useEditor()
|
||||
|
||||
const cb = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
const onlySelectedShape = editor.getOnlySelectedShape()
|
||||
|
||||
if (onlySelectedShape && editor.isShapeOrAncestorLocked(onlySelectedShape)) {
|
||||
editor.setSelectedShapes([])
|
||||
}
|
||||
} else {
|
||||
// Weird route: selecting locked shapes on long press
|
||||
if (editor.getInstanceState().isCoarsePointer) {
|
||||
const selectedShapes = editor.getSelectedShapes()
|
||||
const {
|
||||
inputs: { currentPagePoint },
|
||||
} = editor
|
||||
|
||||
// get all of the shapes under the current pointer
|
||||
const shapesAtPoint = editor.getShapesAtPoint(currentPagePoint)
|
||||
|
||||
if (
|
||||
// if there are no selected shapes
|
||||
!editor.getSelectedShapes().length ||
|
||||
// OR if none of the shapes at the point include the selected shape
|
||||
!shapesAtPoint.some((s) => selectedShapes.includes(s))
|
||||
) {
|
||||
// then are there any locked shapes under the current pointer?
|
||||
const lockedShapes = shapesAtPoint.filter((s) => editor.isShapeOrAncestorLocked(s))
|
||||
|
||||
if (lockedShapes.length) {
|
||||
// nice, let's select them
|
||||
editor.select(...lockedShapes.map((s) => s.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
||||
const container = useContainer()
|
||||
const [isOpen, handleOpenChange] = useMenuIsOpen('context menu', cb)
|
||||
|
||||
// Get the context menu content, either the default component or the user's
|
||||
// override. If there's no menu content, then the user has set it to null,
|
||||
// so skip rendering the menu.
|
||||
const content = children ?? <DefaultContextMenuContent />
|
||||
|
||||
return (
|
||||
<_ContextMenu.Root dir="ltr" onOpenChange={handleOpenChange} modal={false}>
|
||||
<_ContextMenu.Trigger onContextMenu={undefined} dir="ltr">
|
||||
{canvas}
|
||||
</_ContextMenu.Trigger>
|
||||
{isOpen && (
|
||||
<_ContextMenu.Portal container={container}>
|
||||
<_ContextMenu.Content
|
||||
className="tlui-menu scrollable"
|
||||
data-testid="context-menu"
|
||||
alignOffset={-4}
|
||||
collisionPadding={4}
|
||||
onContextMenu={preventDefault}
|
||||
>
|
||||
<TldrawUiMenuContextProvider type="context-menu" sourceId="context-menu">
|
||||
{content}
|
||||
</TldrawUiMenuContextProvider>
|
||||
</_ContextMenu.Content>
|
||||
</_ContextMenu.Portal>
|
||||
)}
|
||||
</_ContextMenu.Root>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,58 @@
|
|||
import { useEditor, useValue } from '@tldraw/editor'
|
||||
import {
|
||||
ArrangeMenuSubmenu,
|
||||
ClipboardMenuGroup,
|
||||
ConversionsMenuGroup,
|
||||
DeleteGroup,
|
||||
DuplicateMenuItem,
|
||||
EditLinkMenuItem,
|
||||
EmbedsGroup,
|
||||
FitFrameToContentMenuItem,
|
||||
GroupMenuItem,
|
||||
MoveToPageMenu,
|
||||
RemoveFrameMenuItem,
|
||||
ReorderMenuSubmenu,
|
||||
SetSelectionGroup,
|
||||
ToggleAutoSizeMenuItem,
|
||||
ToggleLockMenuItem,
|
||||
UngroupMenuItem,
|
||||
} from '../menu-items'
|
||||
import { TldrawUiMenuGroup } from '../menus/TldrawUiMenuGroup'
|
||||
|
||||
/** @public */
|
||||
export function DefaultContextMenuContent() {
|
||||
const editor = useEditor()
|
||||
|
||||
const selectToolActive = useValue(
|
||||
'isSelectToolActive',
|
||||
() => editor.getCurrentToolId() === 'select',
|
||||
[editor]
|
||||
)
|
||||
|
||||
if (!selectToolActive) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<TldrawUiMenuGroup id="selection">
|
||||
<ToggleAutoSizeMenuItem />
|
||||
<EditLinkMenuItem />
|
||||
<DuplicateMenuItem />
|
||||
<GroupMenuItem />
|
||||
<UngroupMenuItem />
|
||||
<RemoveFrameMenuItem />
|
||||
<FitFrameToContentMenuItem />
|
||||
<ToggleLockMenuItem />
|
||||
</TldrawUiMenuGroup>
|
||||
<EmbedsGroup />
|
||||
<TldrawUiMenuGroup id="modify">
|
||||
<ArrangeMenuSubmenu />
|
||||
<ReorderMenuSubmenu />
|
||||
<MoveToPageMenu />
|
||||
</TldrawUiMenuGroup>
|
||||
<ClipboardMenuGroup />
|
||||
<ConversionsMenuGroup />
|
||||
<SetSelectionGroup />
|
||||
<DeleteGroup />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext'
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRoot,
|
||||
DropdownMenuTrigger,
|
||||
} from '../primitives/DropdownMenu'
|
||||
import { DefaultDebugMenuContent } from './DefaultDebugMenuContent'
|
||||
|
||||
/** @public */
|
||||
export type TLUiDebugMenuProps = {
|
||||
children?: any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function DefaultDebugMenu({ children }: TLUiDebugMenuProps) {
|
||||
const msg = useTranslation()
|
||||
const content = children ?? <DefaultDebugMenuContent />
|
||||
|
||||
return (
|
||||
<DropdownMenuRoot id="debug">
|
||||
<DropdownMenuTrigger type="icon" icon="dots-horizontal" title={msg('debug-panel.more')} />
|
||||
<DropdownMenuContent side="top" align="end" alignOffset={0}>
|
||||
<TldrawUiMenuContextProvider type="menu" sourceId="debug-panel">
|
||||
{content}
|
||||
</TldrawUiMenuContextProvider>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuRoot>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,298 @@
|
|||
import { DialogTitle } from '@radix-ui/react-dialog'
|
||||
import {
|
||||
DebugFlag,
|
||||
Editor,
|
||||
TLShapePartial,
|
||||
createShapeId,
|
||||
debugFlags,
|
||||
featureFlags,
|
||||
hardResetEditor,
|
||||
track,
|
||||
uniqueId,
|
||||
useEditor,
|
||||
} from '@tldraw/editor'
|
||||
import React from 'react'
|
||||
import { useDialogs } from '../../context/dialogs'
|
||||
import { useToasts } from '../../context/toasts'
|
||||
import { untranslated } from '../../hooks/useTranslation/useTranslation'
|
||||
import { TldrawUiMenuCheckboxItem } from '../menus/TldrawUiMenuCheckboxItem'
|
||||
import { TldrawUiMenuGroup } from '../menus/TldrawUiMenuGroup'
|
||||
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem'
|
||||
import { TldrawUiMenuSubmenu } from '../menus/TldrawUiMenuSubmenu'
|
||||
import { Button } from '../primitives/Button'
|
||||
import { DialogBody, DialogCloseButton, DialogFooter, DialogHeader } from '../primitives/Dialog'
|
||||
|
||||
/** @public */
|
||||
export function DefaultDebugMenuContent() {
|
||||
const editor = useEditor()
|
||||
const { addToast } = useToasts()
|
||||
const { addDialog } = useDialogs()
|
||||
const [error, setError] = React.useState<boolean>(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TldrawUiMenuGroup id="items">
|
||||
<TldrawUiMenuItem
|
||||
id="add-toast"
|
||||
onSelect={() => {
|
||||
addToast({
|
||||
id: uniqueId(),
|
||||
title: 'Something happened',
|
||||
description: 'Hey, attend to this thing over here. It might be important!',
|
||||
keepOpen: true,
|
||||
// icon?: string
|
||||
// title?: string
|
||||
// description?: string
|
||||
// actions?: TLUiToastAction[]
|
||||
})
|
||||
addToast({
|
||||
id: uniqueId(),
|
||||
title: 'Something happened',
|
||||
description: 'Hey, attend to this thing over here. It might be important!',
|
||||
keepOpen: true,
|
||||
actions: [
|
||||
{
|
||||
label: 'Primary',
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Normal',
|
||||
type: 'normal',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Danger',
|
||||
type: 'danger',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
],
|
||||
// icon?: string
|
||||
// title?: string
|
||||
// description?: string
|
||||
// actions?: TLUiToastAction[]
|
||||
})
|
||||
addToast({
|
||||
id: uniqueId(),
|
||||
title: 'Something happened',
|
||||
description: 'Hey, attend to this thing over here. It might be important!',
|
||||
keepOpen: true,
|
||||
icon: 'twitter',
|
||||
actions: [
|
||||
{
|
||||
label: 'Primary',
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Normal',
|
||||
type: 'normal',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Danger',
|
||||
type: 'danger',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}}
|
||||
label={untranslated('Show toast')}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="show-dialog"
|
||||
label={'Show dialog'}
|
||||
onSelect={() => {
|
||||
addDialog({
|
||||
component: ({ onClose }) => (
|
||||
<ExampleDialog
|
||||
displayDontShowAgain
|
||||
onCancel={() => onClose()}
|
||||
onContinue={() => onClose()}
|
||||
/>
|
||||
),
|
||||
onClose: () => {
|
||||
void null
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="create-shapes"
|
||||
label={'Create 100 shapes'}
|
||||
onSelect={() => createNShapes(editor, 100)}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="count-nodes"
|
||||
label={'Count shapes / nodes'}
|
||||
onSelect={() => {
|
||||
function countDescendants({ children }: HTMLElement) {
|
||||
let count = 0
|
||||
if (!children.length) return 0
|
||||
for (const el of [...(children as any)]) {
|
||||
count++
|
||||
count += countDescendants(el)
|
||||
}
|
||||
return count
|
||||
}
|
||||
const selectedShapes = editor.getSelectedShapes()
|
||||
const shapes =
|
||||
selectedShapes.length === 0 ? editor.getRenderingShapes() : selectedShapes
|
||||
const elms = shapes.map(
|
||||
(shape) => (document.getElementById(shape.id) as HTMLElement)!.parentElement!
|
||||
)
|
||||
let descendants = elms.length
|
||||
for (const elm of elms) {
|
||||
descendants += countDescendants(elm)
|
||||
}
|
||||
window.alert(`Shapes ${shapes.length}, DOM nodes:${descendants}`)
|
||||
}}
|
||||
/>
|
||||
{(() => {
|
||||
if (error) throw Error('oh no!')
|
||||
})()}
|
||||
<TldrawUiMenuItem id="throw-error" onSelect={() => setError(true)} label={'Throw error'} />
|
||||
<TldrawUiMenuItem id="hard-reset" onSelect={hardResetEditor} label={'Hard reset'} />
|
||||
</TldrawUiMenuGroup>
|
||||
<TldrawUiMenuGroup id="flags">
|
||||
<DebugFlags />
|
||||
<FeatureFlags />
|
||||
</TldrawUiMenuGroup>
|
||||
|
||||
{/* {...children} */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DebugFlags() {
|
||||
const items = Object.values(debugFlags)
|
||||
if (!items.length) return null
|
||||
return (
|
||||
<TldrawUiMenuSubmenu id="debug flags" label="Debug Flags">
|
||||
<TldrawUiMenuGroup id="debug flags">
|
||||
{items.map((flag) => (
|
||||
<DebugFlagToggle key={flag.name} flag={flag} />
|
||||
))}
|
||||
</TldrawUiMenuGroup>
|
||||
</TldrawUiMenuSubmenu>
|
||||
)
|
||||
}
|
||||
|
||||
function FeatureFlags() {
|
||||
const items = Object.values(featureFlags)
|
||||
if (!items.length) return null
|
||||
return (
|
||||
<TldrawUiMenuSubmenu id="feature flags" label="Feature Flags">
|
||||
<TldrawUiMenuGroup id="feature flags">
|
||||
{items.map((flag) => (
|
||||
<DebugFlagToggle key={flag.name} flag={flag} />
|
||||
))}
|
||||
</TldrawUiMenuGroup>
|
||||
</TldrawUiMenuSubmenu>
|
||||
)
|
||||
}
|
||||
|
||||
function ExampleDialog({
|
||||
title = 'title',
|
||||
body = 'hello hello hello',
|
||||
cancel = 'Cancel',
|
||||
confirm = 'Continue',
|
||||
displayDontShowAgain = false,
|
||||
onCancel,
|
||||
onContinue,
|
||||
}: {
|
||||
title?: string
|
||||
body?: string
|
||||
cancel?: string
|
||||
confirm?: string
|
||||
displayDontShowAgain?: boolean
|
||||
onCancel: () => void
|
||||
onContinue: () => void
|
||||
}) {
|
||||
const [dontShowAgain, setDontShowAgain] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogCloseButton />
|
||||
</DialogHeader>
|
||||
<DialogBody style={{ maxWidth: 350 }}>{body}</DialogBody>
|
||||
<DialogFooter className="tlui-dialog__footer__actions">
|
||||
{displayDontShowAgain && (
|
||||
<Button
|
||||
type="normal"
|
||||
onClick={() => setDontShowAgain(!dontShowAgain)}
|
||||
iconLeft={dontShowAgain ? 'check' : 'checkbox-empty'}
|
||||
style={{ marginRight: 'auto' }}
|
||||
>
|
||||
{`Don't show again`}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="normal" onClick={onCancel}>
|
||||
{cancel}
|
||||
</Button>
|
||||
<Button type="primary" onClick={async () => onContinue()}>
|
||||
{confirm}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const DebugFlagToggle = track(function DebugFlagToggle({
|
||||
flag,
|
||||
onChange,
|
||||
}: {
|
||||
flag: DebugFlag<boolean>
|
||||
onChange?: (newValue: boolean) => void
|
||||
}) {
|
||||
const value = flag.get()
|
||||
return (
|
||||
<TldrawUiMenuCheckboxItem
|
||||
id={flag.name}
|
||||
title={flag.name}
|
||||
label={flag.name
|
||||
.replace(/([a-z0-9])([A-Z])/g, (m) => `${m[0]} ${m[1].toLowerCase()}`)
|
||||
.replace(/^[a-z]/, (m) => m.toUpperCase())}
|
||||
checked={value}
|
||||
onSelect={() => {
|
||||
flag.set(!value)
|
||||
onChange?.(!value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
let t = 0
|
||||
|
||||
function createNShapes(editor: Editor, n: number) {
|
||||
const shapesToCreate: TLShapePartial[] = Array(n)
|
||||
const cols = Math.floor(Math.sqrt(n))
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
t++
|
||||
shapesToCreate[i] = {
|
||||
id: createShapeId('box' + t),
|
||||
type: 'geo',
|
||||
x: (i % cols) * 132,
|
||||
y: Math.floor(i / cols) * 132,
|
||||
}
|
||||
}
|
||||
|
||||
editor.batch(() => {
|
||||
editor.createShapes(shapesToCreate).setSelectedShapes(shapesToCreate.map((s) => s.id))
|
||||
})
|
||||
}
|
|
@ -1,76 +1,24 @@
|
|||
import {
|
||||
createShapeId,
|
||||
DebugFlag,
|
||||
debugFlags,
|
||||
Editor,
|
||||
featureFlags,
|
||||
hardResetEditor,
|
||||
TLShapePartial,
|
||||
track,
|
||||
uniqueId,
|
||||
useEditor,
|
||||
useValue,
|
||||
Vec,
|
||||
} from '@tldraw/editor'
|
||||
import * as React from 'react'
|
||||
import { useDialogs } from '../hooks/useDialogsProvider'
|
||||
import { useToasts } from '../hooks/useToastsProvider'
|
||||
import { untranslated, useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { Button } from './primitives/Button'
|
||||
import * as Dialog from './primitives/Dialog'
|
||||
import * as DropdownMenu from './primitives/DropdownMenu'
|
||||
|
||||
let t = 0
|
||||
|
||||
function createNShapes(editor: Editor, n: number) {
|
||||
const shapesToCreate: TLShapePartial[] = Array(n)
|
||||
const cols = Math.floor(Math.sqrt(n))
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
t++
|
||||
shapesToCreate[i] = {
|
||||
id: createShapeId('box' + t),
|
||||
type: 'geo',
|
||||
x: (i % cols) * 132,
|
||||
y: Math.floor(i / cols) * 132,
|
||||
}
|
||||
}
|
||||
|
||||
editor.batch(() => {
|
||||
editor.createShapes(shapesToCreate).setSelectedShapes(shapesToCreate.map((s) => s.id))
|
||||
})
|
||||
}
|
||||
import { debugFlags, track, useEditor, useValue, Vec } from '@tldraw/editor'
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import { useTldrawUiComponents } from '../context/components'
|
||||
|
||||
/** @internal */
|
||||
export const DebugPanel = React.memo(function DebugPanel({
|
||||
renderDebugMenuItems,
|
||||
}: {
|
||||
renderDebugMenuItems: (() => React.ReactNode) | null
|
||||
}) {
|
||||
const msg = useTranslation()
|
||||
const showFps = useValue('show_fps', () => debugFlags.showFps.get(), [debugFlags])
|
||||
export const DebugPanel = memo(function DebugPanel() {
|
||||
const { DebugMenu } = useTldrawUiComponents()
|
||||
|
||||
return (
|
||||
<div className="tlui-debug-panel">
|
||||
<CurrentState />
|
||||
{showFps && <FPS />}
|
||||
<ShapeCount />
|
||||
<DropdownMenu.Root id="debug">
|
||||
<DropdownMenu.Trigger>
|
||||
<Button type="icon" icon="dots-horizontal" title={msg('debug-panel.more')} />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content side="top" align="end" alignOffset={0}>
|
||||
<DebugMenuContent renderDebugMenuItems={renderDebugMenuItems} />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<FPS />
|
||||
{DebugMenu && <DebugMenu />}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
function useTick(isEnabled = true) {
|
||||
const [_, setTick] = React.useState(0)
|
||||
const [_, setTick] = useState(0)
|
||||
const editor = useEditor()
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!isEnabled) return
|
||||
const update = () => setTick((tick) => tick + 1)
|
||||
editor.on('tick', update)
|
||||
|
@ -107,9 +55,13 @@ const CurrentState = track(function CurrentState() {
|
|||
})
|
||||
|
||||
function FPS() {
|
||||
const fpsRef = React.useRef<HTMLDivElement>(null)
|
||||
const showFps = useValue('show_fps', () => debugFlags.showFps.get(), [debugFlags])
|
||||
|
||||
const fpsRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!showFps) return
|
||||
|
||||
React.useEffect(() => {
|
||||
const TICK_LENGTH = 250
|
||||
let maxKnownFps = 0
|
||||
let cancelled = false
|
||||
|
@ -168,294 +120,9 @@ function FPS() {
|
|||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
}, [showFps])
|
||||
|
||||
if (!showFps) return null
|
||||
|
||||
return <div ref={fpsRef} />
|
||||
}
|
||||
|
||||
const ShapeCount = function ShapeCount() {
|
||||
const editor = useEditor()
|
||||
const count = useValue('rendering shapes count', () => editor.getRenderingShapes().length, [
|
||||
editor,
|
||||
])
|
||||
|
||||
return <div>{count} Shapes</div>
|
||||
}
|
||||
|
||||
const DebugMenuContent = track(function DebugMenuContent({
|
||||
renderDebugMenuItems,
|
||||
}: {
|
||||
renderDebugMenuItems: (() => React.ReactNode) | null
|
||||
}) {
|
||||
const editor = useEditor()
|
||||
const { addToast } = useToasts()
|
||||
const { addDialog } = useDialogs()
|
||||
const [error, setError] = React.useState<boolean>(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item
|
||||
type="menu"
|
||||
onClick={() => {
|
||||
addToast({
|
||||
id: uniqueId(),
|
||||
title: 'Something happened',
|
||||
description: 'Hey, attend to this thing over here. It might be important!',
|
||||
keepOpen: true,
|
||||
// icon?: string
|
||||
// title?: string
|
||||
// description?: string
|
||||
// actions?: TLUiToastAction[]
|
||||
})
|
||||
addToast({
|
||||
id: uniqueId(),
|
||||
title: 'Something happened',
|
||||
description: 'Hey, attend to this thing over here. It might be important!',
|
||||
keepOpen: true,
|
||||
actions: [
|
||||
{
|
||||
label: 'Primary',
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Normal',
|
||||
type: 'normal',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Danger',
|
||||
type: 'danger',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
],
|
||||
// icon?: string
|
||||
// title?: string
|
||||
// description?: string
|
||||
// actions?: TLUiToastAction[]
|
||||
})
|
||||
addToast({
|
||||
id: uniqueId(),
|
||||
title: 'Something happened',
|
||||
description: 'Hey, attend to this thing over here. It might be important!',
|
||||
keepOpen: true,
|
||||
icon: 'twitter',
|
||||
actions: [
|
||||
{
|
||||
label: 'Primary',
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Normal',
|
||||
type: 'normal',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Danger',
|
||||
type: 'danger',
|
||||
onClick: () => {
|
||||
void null
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}}
|
||||
label={untranslated('Show toast')}
|
||||
/>
|
||||
|
||||
<DropdownMenu.Item
|
||||
type="menu"
|
||||
onClick={() => {
|
||||
addDialog({
|
||||
component: ({ onClose }) => (
|
||||
<ExampleDialog
|
||||
displayDontShowAgain
|
||||
onCancel={() => {
|
||||
onClose()
|
||||
}}
|
||||
onContinue={() => {
|
||||
onClose()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
onClose: () => {
|
||||
void null
|
||||
},
|
||||
})
|
||||
}}
|
||||
label={untranslated('Show dialog')}
|
||||
/>
|
||||
<DropdownMenu.Item
|
||||
type="menu"
|
||||
onClick={() => createNShapes(editor, 100)}
|
||||
label={untranslated('Create 100 shapes')}
|
||||
/>
|
||||
<DropdownMenu.Item
|
||||
type="menu"
|
||||
onClick={() => {
|
||||
function countDescendants({ children }: HTMLElement) {
|
||||
let count = 0
|
||||
if (!children.length) return 0
|
||||
for (const el of [...(children as any)]) {
|
||||
count++
|
||||
count += countDescendants(el)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
const selectedShapes = editor.getSelectedShapes()
|
||||
|
||||
const shapes =
|
||||
selectedShapes.length === 0 ? editor.getRenderingShapes() : selectedShapes
|
||||
|
||||
const elms = shapes.map(
|
||||
(shape) => (document.getElementById(shape.id) as HTMLElement)!.parentElement!
|
||||
)
|
||||
|
||||
let descendants = elms.length
|
||||
|
||||
for (const elm of elms) {
|
||||
descendants += countDescendants(elm)
|
||||
}
|
||||
|
||||
window.alert(`Shapes ${shapes.length}, DOM nodes:${descendants}`)
|
||||
}}
|
||||
label={untranslated('Count shapes / nodes')}
|
||||
/>
|
||||
{(() => {
|
||||
if (error) throw Error('oh no!')
|
||||
})()}
|
||||
<DropdownMenu.Item
|
||||
type="menu"
|
||||
onClick={() => {
|
||||
setError(true)
|
||||
}}
|
||||
label={untranslated('Throw error')}
|
||||
/>
|
||||
<DropdownMenu.Item
|
||||
type="menu"
|
||||
onClick={() => {
|
||||
hardResetEditor()
|
||||
}}
|
||||
label={untranslated('Hard reset')}
|
||||
/>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Group>
|
||||
<DebugFlagToggle flag={debugFlags.debugSvg} />
|
||||
<DebugFlagToggle flag={debugFlags.showFps} />
|
||||
<DebugFlagToggle flag={debugFlags.forceSrgb} />
|
||||
<DebugFlagToggle flag={debugFlags.debugGeometry} />
|
||||
<DebugFlagToggle flag={debugFlags.hideShapes} />
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Group>
|
||||
{Object.values(featureFlags).map((flag) => {
|
||||
return <DebugFlagToggle key={flag.name} flag={flag} />
|
||||
})}
|
||||
</DropdownMenu.Group>
|
||||
{renderDebugMenuItems?.()}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
function Toggle({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string
|
||||
value: boolean
|
||||
onChange: (newValue: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
title={untranslated(label)}
|
||||
checked={value}
|
||||
onSelect={() => onChange(!value)}
|
||||
>
|
||||
<span className="tlui-button__label" draggable={false}>
|
||||
{label}
|
||||
</span>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
const DebugFlagToggle = track(function DebugFlagToggle({
|
||||
flag,
|
||||
onChange,
|
||||
}: {
|
||||
flag: DebugFlag<boolean>
|
||||
onChange?: (newValue: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<Toggle
|
||||
label={flag.name
|
||||
.replace(/([a-z0-9])([A-Z])/g, (m) => `${m[0]} ${m[1].toLowerCase()}`)
|
||||
.replace(/^[a-z]/, (m) => m.toUpperCase())}
|
||||
value={flag.get()}
|
||||
onChange={(newValue) => {
|
||||
flag.set(newValue)
|
||||
onChange?.(newValue)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
function ExampleDialog({
|
||||
title = 'title',
|
||||
body = 'hello hello hello',
|
||||
cancel = 'Cancel',
|
||||
confirm = 'Continue',
|
||||
displayDontShowAgain = false,
|
||||
onCancel,
|
||||
onContinue,
|
||||
}: {
|
||||
title?: string
|
||||
body?: string
|
||||
cancel?: string
|
||||
confirm?: string
|
||||
displayDontShowAgain?: boolean
|
||||
onCancel: () => void
|
||||
onContinue: () => void
|
||||
}) {
|
||||
const [dontShowAgain, setDontShowAgain] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.CloseButton />
|
||||
</Dialog.Header>
|
||||
<Dialog.Body style={{ maxWidth: 350 }}>{body}</Dialog.Body>
|
||||
<Dialog.Footer className="tlui-dialog__footer__actions">
|
||||
{displayDontShowAgain && (
|
||||
<Button
|
||||
type="normal"
|
||||
onClick={() => setDontShowAgain(!dontShowAgain)}
|
||||
iconLeft={dontShowAgain ? 'check' : 'checkbox-empty'}
|
||||
style={{ marginRight: 'auto' }}
|
||||
>
|
||||
{`Don't show again`}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="normal" onClick={onCancel}>
|
||||
{cancel}
|
||||
</Button>
|
||||
<Button type="primary" onClick={async () => onContinue()}>
|
||||
{confirm}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as _Dialog from '@radix-ui/react-dialog'
|
||||
import { useContainer } from '@tldraw/editor'
|
||||
import React, { useCallback } from 'react'
|
||||
import { TLUiDialog, useDialogs } from '../hooks/useDialogsProvider'
|
||||
import { TLUiDialog, useDialogs } from '../context/dialogs'
|
||||
|
||||
const Dialog = ({ id, component: ModalContent, onClose }: TLUiDialog) => {
|
||||
const { removeDialog } = useDialogs()
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { track, useEditor } from '@tldraw/editor'
|
||||
import { useRef } from 'react'
|
||||
import { useActions } from '../hooks/useActions'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { Button } from './primitives/Button'
|
||||
import { kbdStr } from './primitives/shared'
|
||||
|
||||
export const DuplicateButton = track(function DuplicateButton() {
|
||||
const editor = useEditor()
|
||||
const actions = useActions()
|
||||
const msg = useTranslation()
|
||||
const action = actions['duplicate']
|
||||
const ref = useRef<HTMLButtonElement>(null)
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={action.icon}
|
||||
type="icon"
|
||||
onClick={() => action.onSelect('quick-actions')}
|
||||
disabled={!(editor.isIn('select') && editor.getSelectedShapeIds().length > 0)}
|
||||
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
|
||||
smallIcon
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
})
|
|
@ -1,9 +1,10 @@
|
|||
import { DialogTitle } from '@radix-ui/react-dialog'
|
||||
import { T, TLBaseShape, track, useEditor } from '@tldraw/editor'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { TLUiDialogProps } from '../hooks/useDialogsProvider'
|
||||
import { TLUiDialogProps } from '../context/dialogs'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { Button } from './primitives/Button'
|
||||
import * as Dialog from './primitives/Dialog'
|
||||
import { DialogBody, DialogCloseButton, DialogFooter, DialogHeader } from './primitives/Dialog'
|
||||
import { Input } from './primitives/Input'
|
||||
|
||||
// A url can either be invalid, or valid with a protocol, or valid without a protocol.
|
||||
|
@ -133,36 +134,32 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{msg('edit-link-dialog.title')}</Dialog.Title>
|
||||
<Dialog.CloseButton />
|
||||
</Dialog.Header>
|
||||
<Dialog.Body>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{msg('edit-link-title')}</DialogTitle>
|
||||
<DialogCloseButton />
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<div className="tlui-edit-link-dialog">
|
||||
<Input
|
||||
ref={rInput}
|
||||
className="tlui-edit-link-dialog__input"
|
||||
label="edit-link-dialog.url"
|
||||
label="edit-link-url"
|
||||
autofocus
|
||||
value={urlInputState.actual}
|
||||
onValueChange={handleChange}
|
||||
onComplete={handleComplete}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
<div>
|
||||
{urlInputState.valid
|
||||
? msg('edit-link-dialog.detail')
|
||||
: msg('edit-link-dialog.invalid-url')}
|
||||
<div>{urlInputState.valid ? msg('edit-link-detail') : msg('edit-link-invalid-url')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Body>
|
||||
<Dialog.Footer className="tlui-dialog__footer__actions">
|
||||
</DialogBody>
|
||||
<DialogFooter className="tlui-dialog__footer__actions">
|
||||
<Button type="normal" onClick={handleCancel} onTouchEnd={handleCancel}>
|
||||
{msg('edit-link-dialog.cancel')}
|
||||
{msg('edit-link-cancel')}
|
||||
</Button>
|
||||
{isRemoving ? (
|
||||
<Button type={'danger'} onTouchEnd={handleClear} onClick={handleClear}>
|
||||
{msg('edit-link-dialog.clear')}
|
||||
{msg('edit-link-clear')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
@ -171,10 +168,10 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
|
|||
onTouchEnd={handleComplete}
|
||||
onClick={handleComplete}
|
||||
>
|
||||
{msg('edit-link-dialog.save')}
|
||||
{msg('edit-link-save')}
|
||||
</Button>
|
||||
)}
|
||||
</Dialog.Footer>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { DialogTitle } from '@radix-ui/react-dialog'
|
||||
import { EMBED_DEFINITIONS, EmbedDefinition, track, useEditor } from '@tldraw/editor'
|
||||
import { useRef, useState } from 'react'
|
||||
import { TLEmbedResult, getEmbedInfo } from '../../utils/embeds/embeds'
|
||||
import { useAssetUrls } from '../hooks/useAssetUrls'
|
||||
import { TLUiDialogProps } from '../hooks/useDialogsProvider'
|
||||
import { useAssetUrls } from '../context/asset-urls'
|
||||
import { TLUiDialogProps } from '../context/dialogs'
|
||||
import { untranslated, useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { Button } from './primitives/Button'
|
||||
import * as Dialog from './primitives/Dialog'
|
||||
import { DialogBody, DialogCloseButton, DialogFooter, DialogHeader } from './primitives/Dialog'
|
||||
import { Icon } from './primitives/Icon'
|
||||
import { Input } from './primitives/Input'
|
||||
|
||||
|
@ -29,20 +30,20 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{embedDefinition
|
||||
? `${msg('embed-dialog.title')} — ${embedDefinition.title}`
|
||||
: msg('embed-dialog.title')}
|
||||
</Dialog.Title>
|
||||
<Dialog.CloseButton />
|
||||
</Dialog.Header>
|
||||
? `${msg('embed-title')} — ${embedDefinition.title}`
|
||||
: msg('embed-title')}
|
||||
</DialogTitle>
|
||||
<DialogCloseButton />
|
||||
</DialogHeader>
|
||||
{embedDefinition ? (
|
||||
<>
|
||||
<Dialog.Body className="tlui-embed-dialog__enter">
|
||||
<DialogBody className="tlui-embed-dialog__enter">
|
||||
<Input
|
||||
className="tlui-embed-dialog__input"
|
||||
label="embed-dialog.url"
|
||||
label="embed-url"
|
||||
placeholder="http://example.com"
|
||||
autofocus
|
||||
onValueChange={(value) => {
|
||||
|
@ -67,7 +68,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
|||
/>
|
||||
{url === '' ? (
|
||||
<div className="tlui-embed-dialog__instruction">
|
||||
<span>{msg('embed-dialog.instruction')}</span>{' '}
|
||||
<span>{msg('embed-instruction')}</span>{' '}
|
||||
{embedDefinition.instructionLink && (
|
||||
<a
|
||||
target="_blank"
|
||||
|
@ -82,11 +83,11 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
|||
</div>
|
||||
) : (
|
||||
<div className="tlui-embed-dialog__warning">
|
||||
{showError ? msg('embed-dialog.invalid-url') : '\xa0'}
|
||||
{showError ? msg('embed-invalid-url') : '\xa0'}
|
||||
</div>
|
||||
)}
|
||||
</Dialog.Body>
|
||||
<Dialog.Footer className="tlui-dialog__footer__actions">
|
||||
</DialogBody>
|
||||
<DialogFooter className="tlui-dialog__footer__actions">
|
||||
<Button
|
||||
type="normal"
|
||||
onClick={() => {
|
||||
|
@ -94,14 +95,14 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
|||
setEmbedInfoForUrl(null)
|
||||
setUrl('')
|
||||
}}
|
||||
label="embed-dialog.back"
|
||||
label="embed-back"
|
||||
/>
|
||||
<div className="tlui-embed__spacer" />
|
||||
<Button type="normal" label="embed-dialog.cancel" onClick={onClose} />
|
||||
<Button type="normal" label="embed-cancel" onClick={onClose} />
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!embedInfoForUrl}
|
||||
label="embed-dialog.create"
|
||||
label="embed-create"
|
||||
onClick={() => {
|
||||
if (!embedInfoForUrl) return
|
||||
|
||||
|
@ -115,11 +116,11 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
|||
onClose()
|
||||
}}
|
||||
/>
|
||||
</Dialog.Footer>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Dialog.Body className="tlui-embed-dialog__list">
|
||||
<DialogBody className="tlui-embed-dialog__list">
|
||||
{EMBED_DEFINITIONS.map((def) => {
|
||||
return (
|
||||
<Button
|
||||
|
@ -135,7 +136,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
|||
</Button>
|
||||
)
|
||||
})}
|
||||
</Dialog.Body>
|
||||
</DialogBody>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
import { track, useEditor } from '@tldraw/editor'
|
||||
import * as React from 'react'
|
||||
|
||||
/** @internal */
|
||||
export const HTMLCanvas = track(function HTMLCanvas() {
|
||||
const editor = useEditor()
|
||||
const rCanvas = React.useRef<HTMLCanvasElement>(null)
|
||||
|
||||
const camera = editor.getCamera()
|
||||
const shapes = editor.getCurrentPageShapes()
|
||||
if (rCanvas.current) {
|
||||
const cvs = rCanvas.current
|
||||
const ctx = cvs.getContext('2d')!
|
||||
ctx.resetTransform()
|
||||
ctx.clearRect(0, 0, cvs.width, cvs.height)
|
||||
|
||||
const path = new Path2D()
|
||||
|
||||
ctx.translate(camera.x, camera.y)
|
||||
|
||||
for (const shape of shapes) {
|
||||
const bounds = editor.getShapePageBounds(shape)!
|
||||
path.rect(bounds.minX, bounds.minY, bounds.width, bounds.height)
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#cccccc'
|
||||
ctx.fill(path)
|
||||
|
||||
// for (const shape of shapes) {
|
||||
// ctx.save()
|
||||
// const corners = editor.getPageCorners(shape)
|
||||
// corners.forEach((corner) => dot(ctx, corner.x, corner.y, 'red'))
|
||||
// ctx.restore()
|
||||
// }
|
||||
}
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={rCanvas}
|
||||
width={editor.getViewportScreenBounds().width}
|
||||
height={editor.getViewportScreenBounds().height}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
// function dot(ctx: CanvasRenderingContext2D, x: number, y: number, color = '#000') {
|
||||
// ctx.save()
|
||||
// ctx.beginPath()
|
||||
// ctx.ellipse(x, y, 4, 4, 0, 0, Math.PI * 2)
|
||||
// ctx.fillStyle = color
|
||||
// ctx.fill()
|
||||
// ctx.restore()
|
||||
// }
|
|
@ -1,109 +0,0 @@
|
|||
import { Content, Portal, Root, Trigger } from '@radix-ui/react-dropdown-menu'
|
||||
import { useContainer } from '@tldraw/editor'
|
||||
import * as React from 'react'
|
||||
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||
import { useHelpMenuSchema } from '../hooks/useHelpMenuSchema'
|
||||
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
|
||||
import { useReadonly } from '../hooks/useReadonly'
|
||||
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { TLUiIconType } from '../icon-types'
|
||||
import { LanguageMenu } from './LanguageMenu'
|
||||
import { Button } from './primitives/Button'
|
||||
import * as M from './primitives/DropdownMenu'
|
||||
|
||||
interface HelpMenuLink {
|
||||
label: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
|
||||
icon: TLUiIconType | Exclude<string, TLUiIconType>
|
||||
url: string
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface HelpMenuProps {
|
||||
links?: HelpMenuLink[]
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const HelpMenu = React.memo(function HelpMenu() {
|
||||
const container = useContainer()
|
||||
const msg = useTranslation()
|
||||
const [isOpen, onOpenChange] = useMenuIsOpen('help menu')
|
||||
|
||||
return (
|
||||
<div className="tlui-help-menu">
|
||||
<Root dir="ltr" open={isOpen} onOpenChange={onOpenChange} modal={false}>
|
||||
<Trigger asChild dir="ltr">
|
||||
<Button
|
||||
type="help"
|
||||
className="tlui-button"
|
||||
smallIcon
|
||||
title={msg('help-menu.title')}
|
||||
icon="question-mark"
|
||||
/>
|
||||
</Trigger>
|
||||
<Portal container={container}>
|
||||
<Content
|
||||
className="tlui-menu"
|
||||
side="top"
|
||||
sideOffset={8}
|
||||
align="end"
|
||||
alignOffset={0}
|
||||
collisionPadding={4}
|
||||
>
|
||||
<HelpMenuContent />
|
||||
</Content>
|
||||
</Portal>
|
||||
</Root>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
function HelpMenuContent() {
|
||||
const menuSchema = useHelpMenuSchema()
|
||||
|
||||
const isReadonly = useReadonly()
|
||||
|
||||
function getHelpMenuItem(item: TLUiMenuChild) {
|
||||
if (!item) return null
|
||||
if (isReadonly && !item.readonlyOk) return null
|
||||
|
||||
switch (item.type) {
|
||||
case 'custom': {
|
||||
if (item.id === 'LANGUAGE_MENU') {
|
||||
return <LanguageMenu key="item" />
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'group': {
|
||||
return (
|
||||
<M.Group size="small" key={item.id}>
|
||||
{item.children.map(getHelpMenuItem)}
|
||||
</M.Group>
|
||||
)
|
||||
}
|
||||
case 'submenu': {
|
||||
return (
|
||||
<M.Sub id={`help menu ${item.id}`} key={item.id}>
|
||||
<M.SubTrigger label={item.label} />
|
||||
<M.SubContent>{item.children.map(getHelpMenuItem)}</M.SubContent>
|
||||
</M.Sub>
|
||||
)
|
||||
}
|
||||
case 'item': {
|
||||
const { id, kbd, label, onSelect, icon } = item.actionItem
|
||||
return (
|
||||
<M.Item
|
||||
type="menu"
|
||||
key={id}
|
||||
kbd={kbd}
|
||||
label={label}
|
||||
onClick={() => onSelect('help-menu')}
|
||||
iconLeft={icon}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <>{menuSchema.map(getHelpMenuItem)}</>
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { memo } from 'react'
|
||||
import { useBreakpoint } from '../../context/breakpoints'
|
||||
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext'
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRoot,
|
||||
DropdownMenuTrigger,
|
||||
} from '../primitives/DropdownMenu'
|
||||
import { DefaultHelpMenuContent } from './DefaultHelpMenuContent'
|
||||
|
||||
/** @public */
|
||||
export type TLUiHelpMenuProps = {
|
||||
children?: any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const DefaultHelpMenu = memo(function DefaultHelpMenu({ children }: TLUiHelpMenuProps) {
|
||||
const msg = useTranslation()
|
||||
const breakpoint = useBreakpoint()
|
||||
|
||||
// Get the help menu content, either the default component or the user's
|
||||
// override. If there's no menu content, then the user has set it to null,
|
||||
// so skip rendering the menu.
|
||||
const content = children ?? <DefaultHelpMenuContent />
|
||||
|
||||
if (breakpoint < 4) return null
|
||||
|
||||
return (
|
||||
<div className="tlui-help-menu">
|
||||
<DropdownMenuRoot id="help menu">
|
||||
<DropdownMenuTrigger
|
||||
type="help"
|
||||
smallIcon
|
||||
title={msg('help-menu.title')}
|
||||
icon="question-mark"
|
||||
/>
|
||||
<DropdownMenuContent side="top" align="end" alignOffset={0} sideOffset={8}>
|
||||
<TldrawUiMenuContextProvider type="menu" sourceId="help-menu">
|
||||
{content}
|
||||
</TldrawUiMenuContextProvider>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuRoot>
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,32 @@
|
|||
import { useTldrawUiComponents } from '../../context/components'
|
||||
import { useDialogs } from '../../context/dialogs'
|
||||
import { LanguageMenu } from '../LanguageMenu'
|
||||
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem'
|
||||
|
||||
/** @public */
|
||||
export function DefaultHelpMenuContent() {
|
||||
return (
|
||||
<>
|
||||
<LanguageMenu />
|
||||
<KeyboardShortcutsMenuItem />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function KeyboardShortcutsMenuItem() {
|
||||
const { KeyboardShortcutsDialog } = useTldrawUiComponents()
|
||||
const { addDialog } = useDialogs()
|
||||
|
||||
if (!KeyboardShortcutsDialog) return null
|
||||
|
||||
return (
|
||||
<TldrawUiMenuItem
|
||||
id="keyboard-shortcuts"
|
||||
label="help-menu.keyboard-shortcuts"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
addDialog({ component: KeyboardShortcutsDialog })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
import { useEditor } from '@tldraw/editor'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useActions } from '../hooks/useActions'
|
||||
import { Button } from './primitives/Button'
|
||||
import { useActions } from '../../context/actions'
|
||||
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem'
|
||||
|
||||
export function BackToContent() {
|
||||
const editor = useEditor()
|
||||
|
||||
const actions = useActions()
|
||||
const action = actions['back-to-content']
|
||||
|
||||
const [showBackToContent, setShowBackToContent] = useState(false)
|
||||
|
||||
|
@ -42,12 +41,10 @@ export function BackToContent() {
|
|||
if (!showBackToContent) return null
|
||||
|
||||
return (
|
||||
<Button
|
||||
iconLeft={action.icon}
|
||||
label={action.label}
|
||||
type="low"
|
||||
onClick={() => {
|
||||
action.onSelect('helper-buttons')
|
||||
<TldrawUiMenuItem
|
||||
{...actions['back-to-content']}
|
||||
onSelect={() => {
|
||||
actions['back-to-content'].onSelect('helper-buttons')
|
||||
setShowBackToContent(false)
|
||||
}}
|
||||
/>
|
|
@ -0,0 +1,19 @@
|
|||
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext'
|
||||
import { DefaultHelperButtonsContent } from './DefaultHelperButtonsContent'
|
||||
|
||||
/** @public */
|
||||
export type TLUiHelperButtonsProps = {
|
||||
children?: any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function DefaultHelperButtons({ children }: TLUiHelperButtonsProps) {
|
||||
const content = children ?? <DefaultHelperButtonsContent />
|
||||
return (
|
||||
<div className="tlui-helper-buttons">
|
||||
<TldrawUiMenuContextProvider type="helper-buttons" sourceId="helper-buttons">
|
||||
{content}
|
||||
</TldrawUiMenuContextProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue