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
|
apps/docs/.next
|
||||||
|
|
||||||
packages/tldraw/tldraw.css
|
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',
|
icon: 'color',
|
||||||
label: 'tools.card',
|
label: 'tools.card',
|
||||||
kbd: 'c',
|
kbd: 'c',
|
||||||
readonlyOk: false,
|
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
// Whatever you want to happen when the tool is selected
|
// Whatever you want to happen when the tool is selected
|
||||||
editor.setCurrentTool('card')
|
editor.setCurrentTool('card')
|
||||||
|
|
|
@ -155,33 +155,35 @@ The [Tldraw](?) component combines two lower-level components: [TldrawEditor](?)
|
||||||
|
|
||||||
### Customize the default components
|
### 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
|
```tsx
|
||||||
<Tldraw
|
|
||||||
components={{
|
const components: TLComponents = {
|
||||||
Background: YourCustomBackground,
|
Background: YourCustomBackground,
|
||||||
SvgDefs: YourCustomSvgDefs,
|
SvgDefs: YourCustomSvgDefs,
|
||||||
Brush: YourCustomBrush,
|
Brush: YourCustomBrush,
|
||||||
ZoomBrush: YourCustomBrush,
|
ZoomBrush: YourCustomBrush,
|
||||||
CollaboratorBrush: YourCustomBrush,
|
CollaboratorBrush: YourCustomBrush,
|
||||||
Cursor: YourCustomCursor,
|
Cursor: YourCustomCursor,
|
||||||
CollaboratorCursor: YourCustomCursor,
|
CollaboratorCursor: YourCustomCursor,
|
||||||
CollaboratorHint: YourCustomCollaboratorHint,
|
CollaboratorHint: YourCustomCollaboratorHint,
|
||||||
CollaboratorShapeIndicator: YourCustomdicator,
|
CollaboratorShapeIndicator: YourCustomdicator,
|
||||||
Grid: YourCustomGrid,
|
Grid: YourCustomGrid,
|
||||||
Scribble: YourCustomScribble,
|
Scribble: YourCustomScribble,
|
||||||
SnapLine: YourCustomSnapLine,
|
SnapLine: YourCustomSnapLine,
|
||||||
Handles: YourCustomHandles,
|
Handles: YourCustomHandles,
|
||||||
Handle: YourCustomHandle,
|
Handle: YourCustomHandle,
|
||||||
CollaboratorScribble: YourCustomScribble,
|
CollaboratorScribble: YourCustomScribble,
|
||||||
ErrorFallback: YourCustomErrorFallback,
|
ErrorFallback: YourCustomErrorFallback,
|
||||||
ShapeErrorFallback: YourCustomShapeErrorFallback,
|
ShapeErrorFallback: YourCustomShapeErrorFallback,
|
||||||
ShapeIndicatorErrorFallback: YourCustomShapeIndicatorErrorFallback,
|
ShapeIndicatorErrorFallback: YourCustomShapeIndicatorErrorFallback,
|
||||||
Spinner: YourCustomSpinner,
|
Spinner: YourCustomSpinner,
|
||||||
SelectionBackground: YourCustomSelectionBackground,
|
SelectionBackground: YourCustomSelectionBackground,
|
||||||
SelectionForeground: YourCustomSelectionForeground,
|
SelectionForeground: YourCustomSelectionForeground,
|
||||||
HoveredShapeIndicator: YourCustomHoveredShapeIndicator,
|
HoveredShapeIndicator: YourCustomHoveredShapeIndicator,
|
||||||
}}
|
// ...
|
||||||
/>
|
}
|
||||||
|
|
||||||
|
<Tldraw components={components}/>
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
import * as Popover from '@radix-ui/react-popover'
|
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 React, { useState } from 'react'
|
||||||
import { useShareMenuIsOpen } from '../hooks/useShareMenuOpen'
|
import { useShareMenuIsOpen } from '../hooks/useShareMenuOpen'
|
||||||
import { SHARE_PROJECT_ACTION, SHARE_SNAPSHOT_ACTION } from '../utils/sharing'
|
import { SHARE_PROJECT_ACTION, SHARE_SNAPSHOT_ACTION } from '../utils/sharing'
|
||||||
|
@ -33,50 +42,39 @@ export const ExportMenu = React.memo(function ExportMenu() {
|
||||||
side="bottom"
|
side="bottom"
|
||||||
sideOffset={6}
|
sideOffset={6}
|
||||||
>
|
>
|
||||||
<div className="tlui-menu__group">
|
<TldrawUiMenuContextProvider type="panel" sourceId="export-menu">
|
||||||
<Button
|
<TldrawUiMenuGroup id="share">
|
||||||
type="menu"
|
<TldrawUiMenuItem {...shareProject} />
|
||||||
label={shareProject.label}
|
<p className="tlui-menu__group tlui-share-zone__details">
|
||||||
icon={'share-1'}
|
{msg('share-menu.fork-note')}
|
||||||
onClick={() => {
|
</p>
|
||||||
shareProject.onSelect('export-menu')
|
</TldrawUiMenuGroup>
|
||||||
}}
|
<TldrawUiMenuGroup id="snapshot">
|
||||||
/>
|
<TldrawUiMenuItem
|
||||||
<p className="tlui-menu__group tlui-share-zone__details">
|
id="copy-to-clipboard"
|
||||||
{msg('share-menu.fork-note')}
|
readonlyOk
|
||||||
</p>
|
icon={didCopySnapshotLink ? 'clipboard-copied' : 'clipboard-copy'}
|
||||||
</div>
|
label={unwrapLabel(shareSnapshot.label)}
|
||||||
<div className="tlui-menu__group">
|
onSelect={async () => {
|
||||||
<Button
|
setIsUploadingSnapshot(true)
|
||||||
type="menu"
|
await shareSnapshot.onSelect('share-menu')
|
||||||
icon={didCopySnapshotLink ? 'clipboard-copied' : 'clipboard-copy'}
|
setIsUploadingSnapshot(false)
|
||||||
label={shareSnapshot.label!}
|
setDidCopySnapshotLink(true)
|
||||||
onClick={async () => {
|
setTimeout(() => setDidCopySnapshotLink(false), 1000)
|
||||||
setIsUploadingSnapshot(true)
|
}}
|
||||||
await shareSnapshot.onSelect('share-menu')
|
spinner={isUploadingSnapshot}
|
||||||
setIsUploadingSnapshot(false)
|
/>
|
||||||
setDidCopySnapshotLink(true)
|
<p className="tlui-menu__group tlui-share-zone__details">
|
||||||
setTimeout(() => setDidCopySnapshotLink(false), 1000)
|
{msg('share-menu.snapshot-link-note')}
|
||||||
}}
|
</p>
|
||||||
spinner={isUploadingSnapshot}
|
</TldrawUiMenuGroup>
|
||||||
/>
|
<TldrawUiMenuGroup id="save">
|
||||||
<p className="tlui-menu__group tlui-share-zone__details">
|
<TldrawUiMenuItem {...saveFileCopyAction} />
|
||||||
{msg('share-menu.snapshot-link-note')}
|
<p className="tlui-menu__group tlui-share-zone__details">
|
||||||
</p>
|
{msg('share-menu.save-note')}
|
||||||
</div>
|
</p>
|
||||||
<div className="tlui-menu__group">
|
</TldrawUiMenuGroup>
|
||||||
<Button
|
</TldrawUiMenuContextProvider>
|
||||||
type="menu"
|
|
||||||
label={saveFileCopyAction.label}
|
|
||||||
icon={'share-2'}
|
|
||||||
onClick={() => {
|
|
||||||
saveFileCopyAction.onSelect('export-menu')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p className="tlui-menu__group tlui-share-zone__details">
|
|
||||||
{msg('share-menu.save-note')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Portal>
|
</Popover.Portal>
|
||||||
</Popover.Root>
|
</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 { useCallback } from 'react'
|
||||||
import { assetUrls } from '../utils/assetUrls'
|
import { assetUrls } from '../utils/assetUrls'
|
||||||
import { createAssetFromUrl } from '../utils/createAssetFromUrl'
|
import { createAssetFromUrl } from '../utils/createAssetFromUrl'
|
||||||
import { linksUiOverrides } from '../utils/links'
|
|
||||||
import { DebugMenuItems } from '../utils/migration/DebugMenuItems'
|
import { DebugMenuItems } from '../utils/migration/DebugMenuItems'
|
||||||
import { LocalMigration } from '../utils/migration/LocalMigration'
|
import { LocalMigration } from '../utils/migration/LocalMigration'
|
||||||
import { SCRATCH_PERSISTENCE_KEY } from '../utils/scratch-persistence-key'
|
import { SCRATCH_PERSISTENCE_KEY } from '../utils/scratch-persistence-key'
|
||||||
import { useSharing } from '../utils/sharing'
|
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 { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
||||||
|
import { LocalFileMenu } from './FileMenu'
|
||||||
|
import { Links } from './Links'
|
||||||
import { ShareMenu } from './ShareMenu'
|
import { ShareMenu } from './ShareMenu'
|
||||||
import { SneakyOnDropOverride } from './SneakyOnDropOverride'
|
import { SneakyOnDropOverride } from './SneakyOnDropOverride'
|
||||||
import { ThemeUpdater } from './ThemeUpdater/ThemeUpdater'
|
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() {
|
export function LocalEditor() {
|
||||||
const handleUiEvent = useHandleUiEvents()
|
const handleUiEvent = useHandleUiEvents()
|
||||||
const sharingUiOverrides = useSharing({ isMultiplayer: false })
|
const sharingUiOverrides = useSharing()
|
||||||
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: false })
|
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: false })
|
||||||
|
|
||||||
const handleMount = useCallback((editor: Editor) => {
|
const handleMount = useCallback((editor: Editor) => {
|
||||||
|
@ -29,19 +85,14 @@ export function LocalEditor() {
|
||||||
persistenceKey={SCRATCH_PERSISTENCE_KEY}
|
persistenceKey={SCRATCH_PERSISTENCE_KEY}
|
||||||
onMount={handleMount}
|
onMount={handleMount}
|
||||||
autoFocus
|
autoFocus
|
||||||
overrides={[sharingUiOverrides, fileSystemUiOverrides, linksUiOverrides]}
|
overrides={[sharingUiOverrides, fileSystemUiOverrides]}
|
||||||
onUiEvent={handleUiEvent}
|
onUiEvent={handleUiEvent}
|
||||||
components={{
|
components={components}
|
||||||
ErrorFallback: ({ error }) => {
|
|
||||||
throw error
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
shareZone={
|
shareZone={
|
||||||
<div className="tlui-share-zone" draggable={false}>
|
<div className="tlui-share-zone" draggable={false}>
|
||||||
<ShareMenu />
|
<ShareMenu />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
renderDebugMenuItems={() => <DebugMenuItems />}
|
|
||||||
inferDarkMode
|
inferDarkMode
|
||||||
>
|
>
|
||||||
<LocalMigration />
|
<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 { useCallback, useEffect } from 'react'
|
||||||
import { useRemoteSyncClient } from '../hooks/useRemoteSyncClient'
|
import { useRemoteSyncClient } from '../hooks/useRemoteSyncClient'
|
||||||
import { UrlStateParams, useUrlState } from '../hooks/useUrlState'
|
import { UrlStateParams, useUrlState } from '../hooks/useUrlState'
|
||||||
import { assetUrls } from '../utils/assetUrls'
|
import { assetUrls } from '../utils/assetUrls'
|
||||||
import { MULTIPLAYER_SERVER } from '../utils/config'
|
import { MULTIPLAYER_SERVER } from '../utils/config'
|
||||||
|
import { CursorChatMenuItem } from '../utils/context-menu/CursorChatMenuItem'
|
||||||
import { createAssetFromFile } from '../utils/createAssetFromFile'
|
import { createAssetFromFile } from '../utils/createAssetFromFile'
|
||||||
import { createAssetFromUrl } from '../utils/createAssetFromUrl'
|
import { createAssetFromUrl } from '../utils/createAssetFromUrl'
|
||||||
import { linksUiOverrides } from '../utils/links'
|
|
||||||
import { useSharing } from '../utils/sharing'
|
import { useSharing } from '../utils/sharing'
|
||||||
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
|
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
|
||||||
import { useCursorChat } from '../utils/useCursorChat'
|
import { CURSOR_CHAT_ACTION, useCursorChat } from '../utils/useCursorChat'
|
||||||
import { useFileSystem } from '../utils/useFileSystem'
|
import { OPEN_FILE_ACTION, SAVE_FILE_COPY_ACTION, useFileSystem } from '../utils/useFileSystem'
|
||||||
import { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
import { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
||||||
import { CursorChatBubble } from './CursorChatBubble'
|
import { CursorChatBubble } from './CursorChatBubble'
|
||||||
import { EmbeddedInIFrameWarning } from './EmbeddedInIFrameWarning'
|
import { EmbeddedInIFrameWarning } from './EmbeddedInIFrameWarning'
|
||||||
|
import { MultiplayerFileMenu } from './FileMenu'
|
||||||
|
import { Links } from './Links'
|
||||||
import { PeopleMenu } from './PeopleMenu/PeopleMenu'
|
import { PeopleMenu } from './PeopleMenu/PeopleMenu'
|
||||||
import { ShareMenu } from './ShareMenu'
|
import { ShareMenu } from './ShareMenu'
|
||||||
import { SneakyOnDropOverride } from './SneakyOnDropOverride'
|
import { SneakyOnDropOverride } from './SneakyOnDropOverride'
|
||||||
import { StoreErrorScreen } from './StoreErrorScreen'
|
import { StoreErrorScreen } from './StoreErrorScreen'
|
||||||
import { ThemeUpdater } from './ThemeUpdater/ThemeUpdater'
|
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({
|
export function MultiplayerEditor({
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
roomSlug,
|
roomSlug,
|
||||||
|
@ -37,7 +97,7 @@ export function MultiplayerEditor({
|
||||||
})
|
})
|
||||||
|
|
||||||
const isEmbedded = useIsEmbedded(roomSlug)
|
const isEmbedded = useIsEmbedded(roomSlug)
|
||||||
const sharingUiOverrides = useSharing({ isMultiplayer: true })
|
const sharingUiOverrides = useSharing()
|
||||||
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
||||||
const cursorChatOverrides = useCursorChat()
|
const cursorChatOverrides = useCursorChat()
|
||||||
|
|
||||||
|
@ -67,19 +127,10 @@ export function MultiplayerEditor({
|
||||||
store={storeWithStatus}
|
store={storeWithStatus}
|
||||||
assetUrls={assetUrls}
|
assetUrls={assetUrls}
|
||||||
onMount={handleMount}
|
onMount={handleMount}
|
||||||
|
overrides={[sharingUiOverrides, fileSystemUiOverrides, cursorChatOverrides]}
|
||||||
initialState={isReadOnly ? 'hand' : 'select'}
|
initialState={isReadOnly ? 'hand' : 'select'}
|
||||||
overrides={[
|
|
||||||
sharingUiOverrides,
|
|
||||||
fileSystemUiOverrides,
|
|
||||||
linksUiOverrides,
|
|
||||||
cursorChatOverrides,
|
|
||||||
]}
|
|
||||||
onUiEvent={handleUiEvent}
|
onUiEvent={handleUiEvent}
|
||||||
components={{
|
components={components}
|
||||||
ErrorFallback: ({ error }) => {
|
|
||||||
throw error
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
topZone={isOffline && <OfflineIndicator />}
|
topZone={isOffline && <OfflineIndicator />}
|
||||||
shareZone={
|
shareZone={
|
||||||
<div className="tlui-share-zone" draggable={false}>
|
<div className="tlui-share-zone" draggable={false}>
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
import * as Popover from '@radix-ui/react-popover'
|
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 React, { useEffect, useState } from 'react'
|
||||||
import { useShareMenuIsOpen } from '../hooks/useShareMenuOpen'
|
import { useShareMenuIsOpen } from '../hooks/useShareMenuOpen'
|
||||||
import { createQRCodeImageDataString } from '../utils/qrcode'
|
import { createQRCodeImageDataString } from '../utils/qrcode'
|
||||||
|
@ -105,114 +114,118 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
||||||
sideOffset={2}
|
sideOffset={2}
|
||||||
alignOffset={4}
|
alignOffset={4}
|
||||||
>
|
>
|
||||||
{shareState.state === 'shared' || shareState.state === 'readonly' ? (
|
<TldrawUiMenuContextProvider type="panel" sourceId="share-menu">
|
||||||
<>
|
{shareState.state === 'shared' || shareState.state === 'readonly' ? (
|
||||||
<button
|
<>
|
||||||
className="tlui-share-zone__qr-code"
|
<button
|
||||||
style={{ backgroundImage: `url(${currentQrCodeUrl})` }}
|
className="tlui-share-zone__qr-code"
|
||||||
title={msg(
|
style={{ backgroundImage: `url(${currentQrCodeUrl})` }}
|
||||||
isReadOnlyLink ? 'share-menu.copy-readonly-link' : 'share-menu.copy-link'
|
title={msg(
|
||||||
)}
|
isReadOnlyLink ? 'share-menu.copy-readonly-link' : 'share-menu.copy-link'
|
||||||
onClick={() => {
|
)}
|
||||||
setDidCopy(true)
|
|
||||||
setTimeout(() => setDidCopy(false), 1000)
|
|
||||||
navigator.clipboard.writeText(currentShareLinkUrl)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="tlui-menu__group">
|
|
||||||
<Button
|
|
||||||
type="menu"
|
|
||||||
icon={didCopy ? 'clipboard-copied' : 'clipboard-copy'}
|
|
||||||
label={isReadOnlyLink ? 'share-menu.copy-readonly-link' : 'share-menu.copy-link'}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDidCopy(true)
|
setDidCopy(true)
|
||||||
setTimeout(() => setDidCopy(false), 750)
|
setTimeout(() => setDidCopy(false), 1000)
|
||||||
navigator.clipboard.writeText(currentShareLinkUrl)
|
navigator.clipboard.writeText(currentShareLinkUrl)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{shareState.state === 'shared' && (
|
<TldrawUiMenuGroup id="copy">
|
||||||
<Button
|
<TldrawUiMenuItem
|
||||||
type="menu"
|
id="copy-to-clipboard"
|
||||||
label="share-menu.readonly-link"
|
readonlyOk
|
||||||
icon={isReadOnlyLink ? 'check' : 'checkbox-empty'}
|
icon={didCopy ? 'clipboard-copied' : 'clipboard-copy'}
|
||||||
onClick={async () => {
|
label={
|
||||||
setIsReadOnlyLink(() => !isReadOnlyLink)
|
isReadOnlyLink ? 'share-menu.copy-readonly-link' : 'share-menu.copy-link'
|
||||||
|
}
|
||||||
|
onSelect={() => {
|
||||||
|
setDidCopy(true)
|
||||||
|
setTimeout(() => setDidCopy(false), 750)
|
||||||
|
navigator.clipboard.writeText(currentShareLinkUrl)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
{shareState.state === 'shared' && (
|
||||||
<p className="tlui-menu__group tlui-share-zone__details">
|
<TldrawUiMenuItem
|
||||||
{msg(
|
id="toggle-read-only"
|
||||||
isReadOnlyLink
|
label="share-menu.readonly-link"
|
||||||
? 'share-menu.copy-readonly-link-note'
|
icon={isReadOnlyLink ? 'check' : 'checkbox-empty'}
|
||||||
: 'share-menu.copy-link-note'
|
onSelect={async () => {
|
||||||
|
setIsReadOnlyLink(() => !isReadOnlyLink)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</p>
|
<p className="tlui-menu__group tlui-share-zone__details">
|
||||||
</div>
|
{msg(
|
||||||
|
isReadOnlyLink
|
||||||
<div className="tlui-menu__group">
|
|
||||||
<Button
|
|
||||||
type="menu"
|
|
||||||
icon={didCopySnapshotLink ? 'clipboard-copied' : 'clipboard-copy'}
|
|
||||||
label={shareSnapshot.label!}
|
|
||||||
onClick={async () => {
|
|
||||||
setIsUploadingSnapshot(true)
|
|
||||||
await shareSnapshot.onSelect('share-menu')
|
|
||||||
setIsUploadingSnapshot(false)
|
|
||||||
setDidCopySnapshotLink(true)
|
|
||||||
setTimeout(() => setDidCopySnapshotLink(false), 1000)
|
|
||||||
}}
|
|
||||||
spinner={isUploadingSnapshot}
|
|
||||||
/>
|
|
||||||
<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="share-menu.share-project"
|
|
||||||
icon="share-1"
|
|
||||||
onClick={async () => {
|
|
||||||
if (isUploading) return
|
|
||||||
setIsUploading(true)
|
|
||||||
await shareProject.onSelect('menu')
|
|
||||||
setIsUploading(false)
|
|
||||||
}}
|
|
||||||
spinner={isUploading}
|
|
||||||
/>
|
|
||||||
<p className="tlui-menu__group tlui-share-zone__details">
|
|
||||||
{msg(
|
|
||||||
shareState.state === 'offline'
|
|
||||||
? 'share-menu.offline-note'
|
|
||||||
: isReadOnlyLink
|
|
||||||
? 'share-menu.copy-readonly-link-note'
|
? 'share-menu.copy-readonly-link-note'
|
||||||
: 'share-menu.copy-link-note'
|
: 'share-menu.copy-link-note'
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</TldrawUiMenuGroup>
|
||||||
<div className="tlui-menu__group">
|
|
||||||
<Button
|
<TldrawUiMenuGroup id="snapshot">
|
||||||
type="menu"
|
<TldrawUiMenuItem
|
||||||
icon={didCopySnapshotLink ? 'clipboard-copied' : 'clipboard-copy'}
|
{...shareSnapshot}
|
||||||
label={shareSnapshot.label!}
|
icon={didCopySnapshotLink ? 'clipboard-copied' : 'clipboard-copy'}
|
||||||
onClick={async () => {
|
onSelect={async () => {
|
||||||
setIsUploadingSnapshot(true)
|
setIsUploadingSnapshot(true)
|
||||||
await shareSnapshot.onSelect('share-menu')
|
await shareSnapshot.onSelect('share-menu')
|
||||||
setIsUploadingSnapshot(false)
|
setIsUploadingSnapshot(false)
|
||||||
setDidCopySnapshotLink(true)
|
setDidCopySnapshotLink(true)
|
||||||
setTimeout(() => setDidCopySnapshotLink(false), 1000)
|
setTimeout(() => setDidCopySnapshotLink(false), 1000)
|
||||||
}}
|
}}
|
||||||
spinner={isUploadingSnapshot}
|
spinner={isUploadingSnapshot}
|
||||||
/>
|
/>
|
||||||
<p className="tlui-menu__group tlui-share-zone__details">
|
<p className="tlui-menu__group tlui-share-zone__details">
|
||||||
{msg('share-menu.snapshot-link-note')}
|
{msg('share-menu.snapshot-link-note')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</TldrawUiMenuGroup>
|
||||||
</>
|
</>
|
||||||
)}
|
) : (
|
||||||
|
<>
|
||||||
|
<TldrawUiMenuGroup id="share">
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
id="share-project"
|
||||||
|
label="share-menu.share-project"
|
||||||
|
icon="share-1"
|
||||||
|
onSelect={async () => {
|
||||||
|
if (isUploading) return
|
||||||
|
setIsUploading(true)
|
||||||
|
await shareProject.onSelect('menu')
|
||||||
|
setIsUploading(false)
|
||||||
|
}}
|
||||||
|
spinner={isUploading}
|
||||||
|
/>
|
||||||
|
<p className="tlui-menu__group tlui-share-zone__details">
|
||||||
|
{msg(
|
||||||
|
shareState.state === 'offline'
|
||||||
|
? 'share-menu.offline-note'
|
||||||
|
: isReadOnlyLink
|
||||||
|
? 'share-menu.copy-readonly-link-note'
|
||||||
|
: 'share-menu.copy-link-note'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</TldrawUiMenuGroup>
|
||||||
|
<TldrawUiMenuGroup id="copy-snapshot-link">
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
id="copy-snapshot-link"
|
||||||
|
icon={didCopySnapshotLink ? 'clipboard-copied' : 'clipboard-copy'}
|
||||||
|
label={unwrapLabel(shareSnapshot.label)}
|
||||||
|
onSelect={async () => {
|
||||||
|
setIsUploadingSnapshot(true)
|
||||||
|
await shareSnapshot.onSelect('share-menu')
|
||||||
|
setIsUploadingSnapshot(false)
|
||||||
|
setDidCopySnapshotLink(true)
|
||||||
|
setTimeout(() => setDidCopySnapshotLink(false), 1000)
|
||||||
|
}}
|
||||||
|
spinner={isUploadingSnapshot}
|
||||||
|
/>
|
||||||
|
<p className="tlui-menu__group tlui-share-zone__details">
|
||||||
|
{msg('share-menu.snapshot-link-note')}
|
||||||
|
</p>
|
||||||
|
</TldrawUiMenuGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TldrawUiMenuContextProvider>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Portal>
|
</Popover.Portal>
|
||||||
</Popover.Root>
|
</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 { UrlStateSync } from '../components/MultiplayerEditor'
|
||||||
import { StoreErrorScreen } from '../components/StoreErrorScreen'
|
import { StoreErrorScreen } from '../components/StoreErrorScreen'
|
||||||
import { useLocalStore } from '../hooks/useLocalStore'
|
import { useLocalStore } from '../hooks/useLocalStore'
|
||||||
import { assetUrls } from '../utils/assetUrls'
|
import { assetUrls } from '../utils/assetUrls'
|
||||||
import { linksUiOverrides } from '../utils/links'
|
|
||||||
import { DebugMenuItems } from '../utils/migration/DebugMenuItems'
|
import { DebugMenuItems } from '../utils/migration/DebugMenuItems'
|
||||||
import { useSharing } from '../utils/sharing'
|
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 { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
||||||
import { ExportMenu } from './ExportMenu'
|
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 = {
|
type SnapshotEditorProps = {
|
||||||
schema: SerializedSchema
|
schema: SerializedSchema
|
||||||
|
@ -17,7 +63,7 @@ type SnapshotEditorProps = {
|
||||||
|
|
||||||
export function SnapshotsEditor(props: SnapshotEditorProps) {
|
export function SnapshotsEditor(props: SnapshotEditorProps) {
|
||||||
const handleUiEvent = useHandleUiEvents()
|
const handleUiEvent = useHandleUiEvents()
|
||||||
const sharingUiOverrides = useSharing({ isMultiplayer: true })
|
const sharingUiOverrides = useSharing()
|
||||||
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
||||||
const storeResult = useLocalStore(props.records, props.schema)
|
const storeResult = useLocalStore(props.records, props.schema)
|
||||||
if (!storeResult?.ok) return <StoreErrorScreen error={new Error(storeResult?.error)} />
|
if (!storeResult?.ok) return <StoreErrorScreen error={new Error(storeResult?.error)} />
|
||||||
|
@ -27,16 +73,12 @@ export function SnapshotsEditor(props: SnapshotEditorProps) {
|
||||||
<Tldraw
|
<Tldraw
|
||||||
assetUrls={assetUrls}
|
assetUrls={assetUrls}
|
||||||
store={storeResult.value}
|
store={storeResult.value}
|
||||||
overrides={[sharingUiOverrides, fileSystemUiOverrides, linksUiOverrides]}
|
overrides={[sharingUiOverrides, fileSystemUiOverrides]}
|
||||||
onUiEvent={handleUiEvent}
|
onUiEvent={handleUiEvent}
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
editor.updateInstanceState({ isReadonly: true })
|
editor.updateInstanceState({ isReadonly: true })
|
||||||
}}
|
}}
|
||||||
components={{
|
components={components}
|
||||||
ErrorFallback: ({ error }) => {
|
|
||||||
throw error
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
shareZone={
|
shareZone={
|
||||||
<div className="tlui-share-zone" draggable={false}>
|
<div className="tlui-share-zone" draggable={false}>
|
||||||
<ExportMenu />
|
<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'
|
import { env } from '../env'
|
||||||
|
|
||||||
const RELEASE_INFO = `${env} ${process.env.NEXT_PUBLIC_TLDRAW_RELEASE_INFO ?? 'unreleased'}`
|
const RELEASE_INFO = `${env} ${process.env.NEXT_PUBLIC_TLDRAW_RELEASE_INFO ?? 'unreleased'}`
|
||||||
|
|
||||||
export function DebugMenuItems() {
|
export function DebugMenuItems() {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Group>
|
<TldrawUiMenuGroup id="release">
|
||||||
<DropdownMenu.Item
|
<TldrawUiMenuItem
|
||||||
type="menu"
|
id="release-info"
|
||||||
onClick={() => {
|
title={`${RELEASE_INFO}`}
|
||||||
|
label="Version"
|
||||||
|
onSelect={() => {
|
||||||
window.alert(`${RELEASE_INFO}`)
|
window.alert(`${RELEASE_INFO}`)
|
||||||
}}
|
}}
|
||||||
title={`${RELEASE_INFO}`}
|
/>
|
||||||
>
|
<TldrawUiMenuItem
|
||||||
Version
|
id="v1"
|
||||||
</DropdownMenu.Item>
|
label="Test v1 content"
|
||||||
<DropdownMenu.Item
|
onSelect={async () => {
|
||||||
type="menu"
|
|
||||||
onClick={async () => {
|
|
||||||
const { writeV1ContentsToIdb } = await import('./writeV1ContentsToIdb')
|
const { writeV1ContentsToIdb } = await import('./writeV1ContentsToIdb')
|
||||||
await writeV1ContentsToIdb()
|
await writeV1ContentsToIdb()
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
Test v1 content
|
</TldrawUiMenuGroup>
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Group>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,7 @@ import {
|
||||||
TLUiOverrides,
|
TLUiOverrides,
|
||||||
TLUiToastsContextType,
|
TLUiToastsContextType,
|
||||||
TLUiTranslationKey,
|
TLUiTranslationKey,
|
||||||
assert,
|
|
||||||
findMenuItem,
|
|
||||||
isShape,
|
isShape,
|
||||||
menuGroup,
|
|
||||||
menuItem,
|
|
||||||
} from '@tldraw/tldraw'
|
} from '@tldraw/tldraw'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
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_PROJECT_ACTION = 'share-project' as const
|
||||||
export const SHARE_SNAPSHOT_ACTION = 'share-snapshot' 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
|
export const FORK_PROJECT_ACTION = 'fork-project' as const
|
||||||
|
|
||||||
const CREATE_SNAPSHOT_ENDPOINT = `/api/snapshots`
|
const CREATE_SNAPSHOT_ENDPOINT = `/api/snapshots`
|
||||||
const SNAPSHOT_UPLOAD_URL = `/api/new-room`
|
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 navigate = useNavigate()
|
||||||
const id = useSearchParams()[0].get('id') ?? undefined
|
const id = useSearchParams()[0].get('id') ?? undefined
|
||||||
const uploadFileToAsset = useMultiplayerAssets(ASSET_UPLOADER_URL)
|
const uploadFileToAsset = useMultiplayerAssets(ASSET_UPLOADER_URL)
|
||||||
|
@ -188,24 +185,8 @@ export function useSharing({ isMultiplayer }: { isMultiplayer: boolean }): TLUiO
|
||||||
}
|
}
|
||||||
return actions
|
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 { useState } from 'react'
|
||||||
import { userPreferences } from './userPreferences'
|
import { userPreferences } from './userPreferences'
|
||||||
|
|
||||||
|
@ -40,14 +49,14 @@ function ConfirmClearDialog({
|
||||||
const [dontShowAgain, setDontShowAgain] = useState(false)
|
const [dontShowAgain, setDontShowAgain] = useState(false)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog.Header>
|
<DialogHeader>
|
||||||
<Dialog.Title>{msg('file-system.confirm-clear.title')}</Dialog.Title>
|
<DialogTitle>{msg('file-system.confirm-clear.title')}</DialogTitle>
|
||||||
<Dialog.CloseButton />
|
<DialogCloseButton />
|
||||||
</Dialog.Header>
|
</DialogHeader>
|
||||||
<Dialog.Body style={{ maxWidth: 350 }}>
|
<DialogBody style={{ maxWidth: 350 }}>
|
||||||
{msg('file-system.confirm-clear.description')}
|
{msg('file-system.confirm-clear.description')}
|
||||||
</Dialog.Body>
|
</DialogBody>
|
||||||
<Dialog.Footer className="tlui-dialog__footer__actions">
|
<DialogFooter className="tlui-dialog__footer__actions">
|
||||||
<Button
|
<Button
|
||||||
type="normal"
|
type="normal"
|
||||||
onClick={() => setDontShowAgain(!dontShowAgain)}
|
onClick={() => setDontShowAgain(!dontShowAgain)}
|
||||||
|
@ -70,7 +79,7 @@ function ConfirmClearDialog({
|
||||||
>
|
>
|
||||||
{msg('file-system.confirm-clear.continue')}
|
{msg('file-system.confirm-clear.continue')}
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</DialogFooter>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
DialogBody,
|
||||||
|
DialogCloseButton,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
TLUiDialogsContextType,
|
TLUiDialogsContextType,
|
||||||
useLocalStorageState,
|
useLocalStorageState,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
|
@ -46,14 +50,12 @@ function ConfirmLeaveDialog({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog.Header>
|
<DialogHeader>
|
||||||
<Dialog.Title>{msg('sharing.confirm-leave.title')}</Dialog.Title>
|
<DialogTitle>{msg('sharing.confirm-leave.title')}</DialogTitle>
|
||||||
<Dialog.CloseButton />
|
<DialogCloseButton />
|
||||||
</Dialog.Header>
|
</DialogHeader>
|
||||||
<Dialog.Body style={{ maxWidth: 350 }}>
|
<DialogBody style={{ maxWidth: 350 }}>{msg('sharing.confirm-leave.description')}</DialogBody>
|
||||||
{msg('sharing.confirm-leave.description')}
|
<DialogFooter className="tlui-dialog__footer__actions">
|
||||||
</Dialog.Body>
|
|
||||||
<Dialog.Footer className="tlui-dialog__footer__actions">
|
|
||||||
<Button
|
<Button
|
||||||
type="normal"
|
type="normal"
|
||||||
onClick={() => setDontShowAgain(!dontShowAgain)}
|
onClick={() => setDontShowAgain(!dontShowAgain)}
|
||||||
|
@ -76,7 +78,7 @@ function ConfirmLeaveDialog({
|
||||||
>
|
>
|
||||||
{msg('sharing.confirm-leave.leave')}
|
{msg('sharing.confirm-leave.leave')}
|
||||||
</Button>
|
</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 { useState } from 'react'
|
||||||
import { userPreferences } from './userPreferences'
|
import { userPreferences } from './userPreferences'
|
||||||
|
|
||||||
|
@ -40,14 +49,14 @@ function ConfirmOpenDialog({
|
||||||
const [dontShowAgain, setDontShowAgain] = useState(false)
|
const [dontShowAgain, setDontShowAgain] = useState(false)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog.Header>
|
<DialogHeader>
|
||||||
<Dialog.Title>{msg('file-system.confirm-open.title')}</Dialog.Title>
|
<DialogTitle>{msg('file-system.confirm-open.title')}</DialogTitle>
|
||||||
<Dialog.CloseButton />
|
<DialogCloseButton />
|
||||||
</Dialog.Header>
|
</DialogHeader>
|
||||||
<Dialog.Body style={{ maxWidth: 350 }}>
|
<DialogBody style={{ maxWidth: 350 }}>
|
||||||
{msg('file-system.confirm-open.description')}
|
{msg('file-system.confirm-open.description')}
|
||||||
</Dialog.Body>
|
</DialogBody>
|
||||||
<Dialog.Footer className="tlui-dialog__footer__actions">
|
<DialogFooter className="tlui-dialog__footer__actions">
|
||||||
<Button
|
<Button
|
||||||
type="normal"
|
type="normal"
|
||||||
onClick={() => setDontShowAgain(!dontShowAgain)}
|
onClick={() => setDontShowAgain(!dontShowAgain)}
|
||||||
|
@ -70,7 +79,7 @@ function ConfirmOpenDialog({
|
||||||
>
|
>
|
||||||
{msg('file-system.confirm-open.open')}
|
{msg('file-system.confirm-open.open')}
|
||||||
</Button>
|
</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 { useMemo } from 'react'
|
||||||
import { useHandleUiEvents } from './useHandleUiEvent'
|
import { useHandleUiEvents } from './useHandleUiEvent'
|
||||||
|
|
||||||
|
@ -27,36 +27,6 @@ export function useCursorChat(): TLUiOverrides {
|
||||||
}
|
}
|
||||||
return actions
|
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]
|
[handleUiEvent]
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,10 +5,6 @@ import {
|
||||||
TLUiActionItem,
|
TLUiActionItem,
|
||||||
TLUiEventHandler,
|
TLUiEventHandler,
|
||||||
TLUiOverrides,
|
TLUiOverrides,
|
||||||
assert,
|
|
||||||
findMenuItem,
|
|
||||||
menuGroup,
|
|
||||||
menuItem,
|
|
||||||
parseAndLoadDocument,
|
parseAndLoadDocument,
|
||||||
serializeTldrawJsonBlob,
|
serializeTldrawJsonBlob,
|
||||||
transact,
|
transact,
|
||||||
|
@ -19,9 +15,9 @@ import { shouldClearDocument } from './shouldClearDocument'
|
||||||
import { shouldOverrideDocument } from './shouldOverrideDocument'
|
import { shouldOverrideDocument } from './shouldOverrideDocument'
|
||||||
import { useHandleUiEvents } from './useHandleUiEvent'
|
import { useHandleUiEvents } from './useHandleUiEvent'
|
||||||
|
|
||||||
const SAVE_FILE_COPY_ACTION = 'save-file-copy'
|
export const SAVE_FILE_COPY_ACTION = 'save-file-copy'
|
||||||
const OPEN_FILE_ACTION = 'open-file'
|
export const OPEN_FILE_ACTION = 'open-file'
|
||||||
const NEW_PROJECT_ACTION = 'new-file'
|
export const NEW_PROJECT_ACTION = 'new-file'
|
||||||
|
|
||||||
const saveFileNames = new WeakMap<TLStore, string>()
|
const saveFileNames = new WeakMap<TLStore, string>()
|
||||||
|
|
||||||
|
@ -92,31 +88,6 @@ export function useFileSystem({ isMultiplayer }: { isMultiplayer: boolean }): TL
|
||||||
}
|
}
|
||||||
return actions
|
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])
|
}, [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 test from '@playwright/test'
|
||||||
import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
|
import { TLShapeId, TLShapePartial } from '@tldraw/tldraw'
|
||||||
import assert from 'assert'
|
|
||||||
import { rename, writeFile } from 'fs/promises'
|
|
||||||
import { setupPage } from '../shared-e2e'
|
|
||||||
|
|
||||||
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', () => {
|
test.describe('Export snapshots', () => {
|
||||||
const snapshots = {
|
const snapshots = {
|
||||||
|
@ -186,50 +189,50 @@ test.describe('Export snapshots', () => {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshotsToTest = Object.entries(snapshots)
|
// const snapshotsToTest = Object.entries(snapshots)
|
||||||
const filteredSnapshots = snapshotsToTest // maybe we filter these down, there are a lot of them
|
// const filteredSnapshots = snapshotsToTest // maybe we filter these down, there are a lot of them
|
||||||
|
|
||||||
for (const [name, shapes] of filteredSnapshots) {
|
// for (const [name, shapes] of filteredSnapshots) {
|
||||||
test(`Exports with ${name} in dark mode`, async ({ browser }) => {
|
// test(`Exports with ${name} in dark mode`, async ({ browser }) => {
|
||||||
const page = await browser.newPage()
|
// const page = await browser.newPage()
|
||||||
await setupPage(page)
|
// await setupPage(page)
|
||||||
await page.evaluate((shapes) => {
|
// await page.evaluate((shapes) => {
|
||||||
editor.user.updateUserPreferences({ isDarkMode: true })
|
// editor.user.updateUserPreferences({ isDarkMode: true })
|
||||||
editor
|
// editor
|
||||||
.updateInstanceState({ exportBackground: false })
|
// .updateInstanceState({ exportBackground: false })
|
||||||
.selectAll()
|
// .selectAll()
|
||||||
.deleteShapes(editor.getSelectedShapeIds())
|
// .deleteShapes(editor.getSelectedShapeIds())
|
||||||
.createShapes(shapes)
|
// .createShapes(shapes)
|
||||||
}, shapes as any)
|
// }, shapes as any)
|
||||||
|
|
||||||
await snapshotTest(page)
|
// await snapshotTest(page)
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function snapshotTest(page: Page) {
|
// async function snapshotTest(page: Page) {
|
||||||
const downloadAndSnapshot = page.waitForEvent('download').then(async (download) => {
|
// const downloadAndSnapshot = page.waitForEvent('download').then(async (download) => {
|
||||||
const path = (await download.path()) as string
|
// const path = (await download.path()) as string
|
||||||
assert(path)
|
// assert(path)
|
||||||
await rename(path, path + '.svg')
|
// await rename(path, path + '.svg')
|
||||||
await writeFile(
|
// await writeFile(
|
||||||
path + '.html',
|
// path + '.html',
|
||||||
`
|
// `
|
||||||
<!DOCTYPE html>
|
// <!DOCTYPE html>
|
||||||
<meta charset="utf-8" />
|
// <meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
// <meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<img src="${path}.svg" />
|
// <img src="${path}.svg" />
|
||||||
`,
|
// `,
|
||||||
'utf-8'
|
// 'utf-8'
|
||||||
)
|
// )
|
||||||
|
|
||||||
await page.goto(`file://${path}.html`)
|
// await page.goto(`file://${path}.html`)
|
||||||
const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
|
// const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
|
||||||
await expect(page).toHaveScreenshot({
|
// await expect(page).toHaveScreenshot({
|
||||||
omitBackground: true,
|
// omitBackground: true,
|
||||||
clip,
|
// clip,
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
await page.evaluate(() => (window as any)['tldraw-export']())
|
// await page.evaluate(() => (window as any)['tldraw-export']())
|
||||||
await downloadAndSnapshot
|
// await downloadAndSnapshot
|
||||||
}
|
// }
|
||||||
})
|
})
|
||||||
|
|
|
@ -46,12 +46,12 @@ test.describe.skip('clipboard tests', () => {
|
||||||
expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
|
expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
|
||||||
|
|
||||||
await page.getByTestId('main.menu').click()
|
await page.getByTestId('main.menu').click()
|
||||||
await page.getByTestId('menu-item.edit').click()
|
await page.getByTestId('main-menu-sub-trigger.edit').click()
|
||||||
await page.getByTestId('menu-item.copy').click()
|
await page.getByTestId('main-menu.copy').click()
|
||||||
await sleep(100)
|
await sleep(100)
|
||||||
await page.getByTestId('main.menu').click()
|
await page.getByTestId('main.menu').click()
|
||||||
await page.getByTestId('menu-item.edit').click()
|
await page.getByTestId('main-menu-sub-trigger.edit').click()
|
||||||
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.getCurrentPageShapes().length)).toBe(2)
|
||||||
expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
|
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)
|
expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
|
||||||
|
|
||||||
await page.mouse.click(100, 100, { button: 'right' })
|
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 sleep(100)
|
||||||
await page.mouse.move(200, 200)
|
await page.mouse.move(200, 200)
|
||||||
await page.mouse.click(100, 100, { button: 'right' })
|
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.getCurrentPageShapes().length)).toBe(2)
|
||||||
expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
|
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.describe('Delete bug', () => {
|
||||||
test.beforeEach(async ({ browser }) => {
|
test.beforeEach(async ({ browser }) => {
|
||||||
page = await browser.newPage()
|
page = await browser.newPage()
|
||||||
|
|
|
@ -26,8 +26,8 @@ test.describe('smoke tests', () => {
|
||||||
|
|
||||||
test('undo and redo', async ({ page }) => {
|
test('undo and redo', async ({ page }) => {
|
||||||
// buttons should be disabled when there is no history
|
// buttons should be disabled when there is no history
|
||||||
expect(page.getByTestId('main.undo')).toBeDisabled()
|
expect(page.getByTestId('quick-actions.undo')).toBeDisabled()
|
||||||
expect(page.getByTestId('main.redo')).toBeDisabled()
|
expect(page.getByTestId('quick-actions.redo')).toBeDisabled()
|
||||||
|
|
||||||
// create a shape
|
// create a shape
|
||||||
await page.keyboard.press('r')
|
await page.keyboard.press('r')
|
||||||
|
@ -39,22 +39,22 @@ test.describe('smoke tests', () => {
|
||||||
expect(await getAllShapeTypes(page)).toEqual(['geo'])
|
expect(await getAllShapeTypes(page)).toEqual(['geo'])
|
||||||
|
|
||||||
// We should have an undoable shape
|
// We should have an undoable shape
|
||||||
expect(page.getByTestId('main.undo')).not.toBeDisabled()
|
expect(page.getByTestId('quick-actions.undo')).not.toBeDisabled()
|
||||||
expect(page.getByTestId('main.redo')).toBeDisabled()
|
expect(page.getByTestId('quick-actions.redo')).toBeDisabled()
|
||||||
|
|
||||||
// Click the undo button to undo the shape
|
// 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(await getAllShapeTypes(page)).toEqual([])
|
||||||
expect(page.getByTestId('main.undo')).toBeDisabled()
|
expect(page.getByTestId('quick-actions.undo')).toBeDisabled()
|
||||||
expect(page.getByTestId('main.redo')).not.toBeDisabled()
|
expect(page.getByTestId('quick-actions.redo')).not.toBeDisabled()
|
||||||
|
|
||||||
// Click the redo button to redo the shape
|
// 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 getAllShapeTypes(page)).toEqual(['geo'])
|
||||||
expect(await page.getByTestId('main.undo').isDisabled()).not.toBe(true)
|
expect(await page.getByTestId('quick-actions.undo').isDisabled()).not.toBe(true)
|
||||||
expect(await page.getByTestId('main.redo').isDisabled()).toBe(true)
|
expect(await page.getByTestId('quick-actions.redo').isDisabled()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('style panel + undo and redo squashing', async ({ page }) => {
|
test('style panel + undo and redo squashing', async ({ page }) => {
|
||||||
|
@ -108,8 +108,8 @@ test.describe('smoke tests', () => {
|
||||||
await page.mouse.up()
|
await page.mouse.up()
|
||||||
|
|
||||||
// Now undo and redo
|
// Now undo and redo
|
||||||
const undo = page.getByTestId('main.undo')
|
const undo = page.getByTestId('quick-actions.undo')
|
||||||
const redo = page.getByTestId('main.redo')
|
const redo = page.getByTestId('quick-actions.redo')
|
||||||
|
|
||||||
await undo.click() // orange -> light blue
|
await undo.click() // orange -> light blue
|
||||||
expect(await getSelectedShapeColor()).toBe('light-blue') // skipping squashed colors!
|
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() // black -> light blue
|
||||||
await redo.click() // light-blue -> orange
|
await redo.click() // light-blue -> orange
|
||||||
|
|
||||||
expect(await page.getByTestId('main.undo').isDisabled()).not.toBe(true)
|
expect(await page.getByTestId('quick-actions.undo').isDisabled()).not.toBe(true)
|
||||||
expect(await page.getByTestId('main.redo').isDisabled()).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!
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
@ -10,7 +18,6 @@ export const uiOverrides: TLUiOverrides = {
|
||||||
icon: 'color',
|
icon: 'color',
|
||||||
label: 'Card',
|
label: 'Card',
|
||||||
kbd: 'c',
|
kbd: 'c',
|
||||||
readonlyOk: false,
|
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
editor.setCurrentTool('card')
|
editor.setCurrentTool('card')
|
||||||
},
|
},
|
||||||
|
@ -22,13 +29,18 @@ export const uiOverrides: TLUiOverrides = {
|
||||||
toolbar.splice(4, 0, toolbarItem(tools.card))
|
toolbar.splice(4, 0, toolbarItem(tools.card))
|
||||||
return toolbar
|
return toolbar
|
||||||
},
|
},
|
||||||
keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
|
}
|
||||||
// Add the tool item from the context to the keyboard shortcuts dialog.
|
|
||||||
const toolsGroup = keyboardShortcutsMenu.find(
|
export const components: TLComponents = {
|
||||||
(group) => group.id === 'shortcuts-dialog.tools'
|
KeyboardShortcutsDialog: (props) => {
|
||||||
) as TLUiMenuGroup
|
const tools = useTools()
|
||||||
toolsGroup.children.push(menuItem(tools.card))
|
return (
|
||||||
return keyboardShortcutsMenu
|
<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 '@tldraw/tldraw/tldraw.css'
|
||||||
import { CardShapeTool, CardShapeUtil } from './CardShape'
|
import { CardShapeTool, CardShapeUtil } from './CardShape'
|
||||||
import { FilterStyleUi } from './FilterStyleUi'
|
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!
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ export default function CustomStylesExample() {
|
||||||
shapeUtils={customShapeUtils}
|
shapeUtils={customShapeUtils}
|
||||||
tools={customTools}
|
tools={customTools}
|
||||||
overrides={uiOverrides}
|
overrides={uiOverrides}
|
||||||
|
components={components}
|
||||||
>
|
>
|
||||||
<FilterStyleUi />
|
<FilterStyleUi />
|
||||||
</Tldraw>
|
</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!
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
@ -9,7 +17,6 @@ export const uiOverrides: TLUiOverrides = {
|
||||||
icon: 'color',
|
icon: 'color',
|
||||||
label: 'Card' as any,
|
label: 'Card' as any,
|
||||||
kbd: 'c',
|
kbd: 'c',
|
||||||
readonlyOk: false,
|
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
editor.setCurrentTool('card')
|
editor.setCurrentTool('card')
|
||||||
},
|
},
|
||||||
|
@ -20,12 +27,19 @@ export const uiOverrides: TLUiOverrides = {
|
||||||
toolbar.splice(4, 0, toolbarItem(tools.card))
|
toolbar.splice(4, 0, toolbarItem(tools.card))
|
||||||
return toolbar
|
return toolbar
|
||||||
},
|
},
|
||||||
keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
|
}
|
||||||
const toolsGroup = keyboardShortcutsMenu.find(
|
|
||||||
(group) => group.id === 'shortcuts-dialog.tools'
|
export const components: TLComponents = {
|
||||||
) as TLUiMenuGroup
|
KeyboardShortcutsDialog: (props) => {
|
||||||
toolsGroup.children.push(menuItem(tools.card))
|
const tools = useTools()
|
||||||
return keyboardShortcutsMenu
|
|
||||||
|
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 {
|
import {
|
||||||
Canvas,
|
Canvas,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
DefaultContextMenuContent,
|
||||||
TldrawEditor,
|
TldrawEditor,
|
||||||
TldrawHandles,
|
TldrawHandles,
|
||||||
TldrawHoveredShapeIndicator,
|
TldrawHoveredShapeIndicator,
|
||||||
|
@ -38,8 +39,8 @@ export default function ExplodedExample() {
|
||||||
persistenceKey="exploded-example"
|
persistenceKey="exploded-example"
|
||||||
>
|
>
|
||||||
<TldrawUi>
|
<TldrawUi>
|
||||||
<ContextMenu>
|
<ContextMenu canvas={<Canvas />}>
|
||||||
<Canvas />
|
<DefaultContextMenuContent />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</TldrawUi>
|
</TldrawUi>
|
||||||
</TldrawEditor>
|
</TldrawEditor>
|
||||||
|
|
|
@ -1,11 +1,4 @@
|
||||||
import {
|
import { TLUiActionsContextType, TLUiOverrides, TLUiToolsContextType, Tldraw } from '@tldraw/tldraw'
|
||||||
TLUiActionsContextType,
|
|
||||||
TLUiMenuGroup,
|
|
||||||
TLUiOverrides,
|
|
||||||
TLUiToolsContextType,
|
|
||||||
Tldraw,
|
|
||||||
menuItem,
|
|
||||||
} from '@tldraw/tldraw'
|
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
import jsonSnapshot from './snapshot.json'
|
import jsonSnapshot from './snapshot.json'
|
||||||
|
|
||||||
|
@ -15,7 +8,6 @@ import jsonSnapshot from './snapshot.json'
|
||||||
const overrides: TLUiOverrides = {
|
const overrides: TLUiOverrides = {
|
||||||
//[a]
|
//[a]
|
||||||
actions(_editor, actions): TLUiActionsContextType {
|
actions(_editor, actions): TLUiActionsContextType {
|
||||||
actions['copy-as-png'].kbd = '$1'
|
|
||||||
actions['toggle-grid'].kbd = 'x'
|
actions['toggle-grid'].kbd = 'x'
|
||||||
return actions
|
return actions
|
||||||
},
|
},
|
||||||
|
@ -24,15 +16,6 @@ const overrides: TLUiOverrides = {
|
||||||
tools['draw'].kbd = 'p'
|
tools['draw'].kbd = 'p'
|
||||||
return tools
|
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]
|
// [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
|
We're overriding the draw tool's shortcut to 'p', maybe we want to rename it to the pen
|
||||||
tool or something.
|
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]
|
[2]
|
||||||
Finally, we pass our overrides object into the Tldraw component's overrides prop. Now when
|
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
|
the component mounts, our overrides will be applied. If you open the keyboard shortcuts
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable import/no-extraneous-dependencies */
|
/* 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 { MiniBoxShapeUtil } from './MiniBoxShape'
|
||||||
import { MiniSelectTool } from './MiniSelectTool'
|
import { MiniSelectTool } from './MiniSelectTool'
|
||||||
|
|
||||||
|
@ -32,24 +32,28 @@ export default function OnlyEditorExample() {
|
||||||
])
|
])
|
||||||
}}
|
}}
|
||||||
components={{
|
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>
|
</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
|
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.
|
create your own custom UI, shape and tool interactions.
|
||||||
|
|
|
@ -26,7 +26,6 @@ const customUiOverrides: TLUiOverrides = {
|
||||||
screenshot: {
|
screenshot: {
|
||||||
id: 'screenshot',
|
id: 'screenshot',
|
||||||
label: 'Screenshot',
|
label: 'Screenshot',
|
||||||
readonlyOk: false,
|
|
||||||
icon: 'tool-screenshot',
|
icon: 'tool-screenshot',
|
||||||
kbd: 'j',
|
kbd: 'j',
|
||||||
onSelect() {
|
onSelect() {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Tldraw } from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
import { SpeechBubbleTool } from './SpeechBubble/SpeechBubbleTool'
|
import { SpeechBubbleTool } from './SpeechBubble/SpeechBubbleTool'
|
||||||
import { SpeechBubbleUtil } from './SpeechBubble/SpeechBubbleUtil'
|
import { SpeechBubbleUtil } from './SpeechBubble/SpeechBubbleUtil'
|
||||||
import { customAssetUrls, uiOverrides } from './SpeechBubble/ui-overrides'
|
import { components, customAssetUrls, uiOverrides } from './SpeechBubble/ui-overrides'
|
||||||
import './customhandles.css'
|
import './customhandles.css'
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
// There's a guide at the bottom of this file!
|
||||||
|
@ -20,6 +20,7 @@ export default function CustomShapeWithHandles() {
|
||||||
tools={tools}
|
tools={tools}
|
||||||
overrides={uiOverrides}
|
overrides={uiOverrides}
|
||||||
assetUrls={customAssetUrls}
|
assetUrls={customAssetUrls}
|
||||||
|
components={components}
|
||||||
persistenceKey="whatever"
|
persistenceKey="whatever"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import {
|
import {
|
||||||
|
DefaultKeyboardShortcutsDialog,
|
||||||
|
DefaultKeyboardShortcutsDialogContent,
|
||||||
|
TLComponents,
|
||||||
TLUiAssetUrlOverrides,
|
TLUiAssetUrlOverrides,
|
||||||
TLUiMenuGroup,
|
|
||||||
TLUiOverrides,
|
TLUiOverrides,
|
||||||
menuItem,
|
TldrawUiMenuItem,
|
||||||
toolbarItem,
|
toolbarItem,
|
||||||
|
useTools,
|
||||||
} from '@tldraw/tldraw'
|
} from '@tldraw/tldraw'
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
// There's a guide at the bottom of this file!
|
||||||
|
@ -16,7 +19,6 @@ export const uiOverrides: TLUiOverrides = {
|
||||||
icon: 'speech-bubble',
|
icon: 'speech-bubble',
|
||||||
label: 'Speech Bubble',
|
label: 'Speech Bubble',
|
||||||
kbd: 's',
|
kbd: 's',
|
||||||
readonlyOk: false,
|
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
editor.setCurrentTool('speech-bubble')
|
editor.setCurrentTool('speech-bubble')
|
||||||
},
|
},
|
||||||
|
@ -27,13 +29,6 @@ export const uiOverrides: TLUiOverrides = {
|
||||||
toolbar.splice(4, 0, toolbarItem(tools.speech))
|
toolbar.splice(4, 0, toolbarItem(tools.speech))
|
||||||
return toolbar
|
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]
|
// [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
|
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
|
// eslint-disable-next-line import/no-internal-modules
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
// eslint-disable-next-line import/no-internal-modules
|
// eslint-disable-next-line import/no-internal-modules
|
||||||
import { getAssetUrlsByImport } from '@tldraw/assets/imports'
|
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 { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { VscodeMessage } from '../../messages'
|
import { VscodeMessage } from '../../messages'
|
||||||
import '../public/index.css'
|
import '../public/index.css'
|
||||||
import { ChangeResponder } from './ChangeResponder'
|
import { ChangeResponder } from './ChangeResponder'
|
||||||
import { FileOpen } from './FileOpen'
|
import { FileOpen } from './FileOpen'
|
||||||
import { FullPageMessage } from './FullPageMessage'
|
import { FullPageMessage } from './FullPageMessage'
|
||||||
|
import { Links } from './Links'
|
||||||
import { onCreateAssetFromUrl } from './utils/bookmarks'
|
import { onCreateAssetFromUrl } from './utils/bookmarks'
|
||||||
import { vscode } from './utils/vscode'
|
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 = () => {
|
export const TldrawWrapper = () => {
|
||||||
const [tldrawInnerProps, setTldrawInnerProps] = useState<TLDrawInnerProps | null>(null)
|
const [tldrawInnerProps, setTldrawInnerProps] = useState<TLDrawInnerProps | null>(null)
|
||||||
|
|
||||||
|
@ -114,6 +105,16 @@ export type TLDrawInnerProps = {
|
||||||
isDarkMode: boolean
|
isDarkMode: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const components: TLComponents = {
|
||||||
|
HelpMenu: () => (
|
||||||
|
<DefaultHelpMenu>
|
||||||
|
<TldrawUiMenuGroup id="help">
|
||||||
|
<DefaultHelpMenuContent />
|
||||||
|
</TldrawUiMenuGroup>
|
||||||
|
<Links />
|
||||||
|
</DefaultHelpMenu>
|
||||||
|
),
|
||||||
|
}
|
||||||
function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerProps) {
|
function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerProps) {
|
||||||
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
|
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
|
||||||
|
|
||||||
|
@ -126,10 +127,11 @@ function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerPro
|
||||||
assetUrls={assetUrls}
|
assetUrls={assetUrls}
|
||||||
persistenceKey={uri}
|
persistenceKey={uri}
|
||||||
onMount={handleMount}
|
onMount={handleMount}
|
||||||
overrides={[menuOverrides, linksUiOverrides]}
|
components={components}
|
||||||
autoFocus
|
autoFocus
|
||||||
>
|
>
|
||||||
{/* <DarkModeHandler themeKind={themeKind} /> */}
|
{/* <DarkModeHandler themeKind={themeKind} /> */}
|
||||||
|
|
||||||
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
|
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
|
||||||
<ChangeResponder />
|
<ChangeResponder />
|
||||||
</Tldraw>
|
</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
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,358 +1,358 @@
|
||||||
{
|
{
|
||||||
"action.convert-to-bookmark": "Pretvori v zaznamek",
|
"action.convert-to-bookmark": "Pretvori v zaznamek",
|
||||||
"action.convert-to-embed": "Pretvori v vdelavo",
|
"action.convert-to-embed": "Pretvori v vdelavo",
|
||||||
"action.open-embed-link": "Odpri povezavo",
|
"action.open-embed-link": "Odpri povezavo",
|
||||||
"action.align-bottom": "Poravnaj dno",
|
"action.align-bottom": "Poravnaj dno",
|
||||||
"action.align-center-horizontal": "Poravnaj vodoravno",
|
"action.align-center-horizontal": "Poravnaj vodoravno",
|
||||||
"action.align-center-vertical": "Poravnaj navpično",
|
"action.align-center-vertical": "Poravnaj navpično",
|
||||||
"action.align-center-horizontal.short": "Poravnaj vodoravno",
|
"action.align-center-horizontal.short": "Poravnaj vodoravno",
|
||||||
"action.align-center-vertical.short": "Poravnaj navpično",
|
"action.align-center-vertical.short": "Poravnaj navpično",
|
||||||
"action.align-left": "Poravnaj levo",
|
"action.align-left": "Poravnaj levo",
|
||||||
"action.align-right": "Poravnaj desno",
|
"action.align-right": "Poravnaj desno",
|
||||||
"action.align-top": "Poravnaj vrh",
|
"action.align-top": "Poravnaj vrh",
|
||||||
"action.back-to-content": "Nazaj na vsebino",
|
"action.back-to-content": "Nazaj na vsebino",
|
||||||
"action.bring-forward": "Premakni naprej",
|
"action.bring-forward": "Premakni naprej",
|
||||||
"action.bring-to-front": "Premakni v ospredje",
|
"action.bring-to-front": "Premakni v ospredje",
|
||||||
"action.copy-as-json.short": "JSON",
|
"action.copy-as-json.short": "JSON",
|
||||||
"action.copy-as-json": "Kopiraj kot JSON",
|
"action.copy-as-json": "Kopiraj kot JSON",
|
||||||
"action.copy-as-png.short": "PNG",
|
"action.copy-as-png.short": "PNG",
|
||||||
"action.copy-as-png": "Kopiraj kot PNG",
|
"action.copy-as-png": "Kopiraj kot PNG",
|
||||||
"action.copy-as-svg.short": "SVG",
|
"action.copy-as-svg.short": "SVG",
|
||||||
"action.copy-as-svg": "Kopiraj kot SVG",
|
"action.copy-as-svg": "Kopiraj kot SVG",
|
||||||
"action.copy": "Kopiraj",
|
"action.copy": "Kopiraj",
|
||||||
"action.cut": "Izreži",
|
"action.cut": "Izreži",
|
||||||
"action.delete": "Izbriši",
|
"action.delete": "Izbriši",
|
||||||
"action.distribute-horizontal": "Porazdeli vodoravno",
|
"action.distribute-horizontal": "Porazdeli vodoravno",
|
||||||
"action.distribute-vertical": "Porazdeli navpično",
|
"action.distribute-vertical": "Porazdeli navpično",
|
||||||
"action.distribute-horizontal.short": "Porazdeli vodoravno",
|
"action.distribute-horizontal.short": "Porazdeli vodoravno",
|
||||||
"action.distribute-vertical.short": "Porazdeli navpično",
|
"action.distribute-vertical.short": "Porazdeli navpično",
|
||||||
"action.duplicate": "Podvoji",
|
"action.duplicate": "Podvoji",
|
||||||
"action.edit-link": "Uredi povezavo",
|
"action.edit-link": "Uredi povezavo",
|
||||||
"action.exit-pen-mode": "Zapustite način peresa",
|
"action.exit-pen-mode": "Zapustite način peresa",
|
||||||
"action.export-as-json.short": "JSON",
|
"action.export-as-json.short": "JSON",
|
||||||
"action.export-as-json": "Izvozi kot JSON",
|
"action.export-as-json": "Izvozi kot JSON",
|
||||||
"action.export-as-png.short": "PNG",
|
"action.export-as-png.short": "PNG",
|
||||||
"action.export-as-png": "Izvozi kot PNG",
|
"action.export-as-png": "Izvozi kot PNG",
|
||||||
"action.export-as-svg.short": "SVG",
|
"action.export-as-svg.short": "SVG",
|
||||||
"action.export-as-svg": "Izvozi kot SVG",
|
"action.export-as-svg": "Izvozi kot SVG",
|
||||||
"action.flip-horizontal": "Zrcali vodoravno",
|
"action.flip-horizontal": "Zrcali vodoravno",
|
||||||
"action.flip-vertical": "Zrcali navpično",
|
"action.flip-vertical": "Zrcali navpično",
|
||||||
"action.flip-horizontal.short": "Zrcali horizontalno",
|
"action.flip-horizontal.short": "Zrcali horizontalno",
|
||||||
"action.flip-vertical.short": "Zrcali vertikalno",
|
"action.flip-vertical.short": "Zrcali vertikalno",
|
||||||
"action.group": "Združi",
|
"action.group": "Združi",
|
||||||
"action.insert-media": "Naloži predstavnost",
|
"action.insert-media": "Naloži predstavnost",
|
||||||
"action.new-shared-project": "Nov skupni projekt",
|
"action.new-shared-project": "Nov skupni projekt",
|
||||||
"action.open-file": "Odpri datoteko",
|
"action.open-file": "Odpri datoteko",
|
||||||
"action.pack": "Spakiraj",
|
"action.pack": "Spakiraj",
|
||||||
"action.paste": "Prilepi",
|
"action.paste": "Prilepi",
|
||||||
"action.print": "Natisni",
|
"action.print": "Natisni",
|
||||||
"action.redo": "Uveljavi",
|
"action.redo": "Uveljavi",
|
||||||
"action.rotate-ccw": "Zavrti v nasprotni smeri urinega kazalca",
|
"action.rotate-ccw": "Zavrti v nasprotni smeri urinega kazalca",
|
||||||
"action.rotate-cw": "Zavrti v smeri urinega kazalca",
|
"action.rotate-cw": "Zavrti v smeri urinega kazalca",
|
||||||
"action.save-copy": "Shrani kopijo",
|
"action.save-copy": "Shrani kopijo",
|
||||||
"action.select-all": "Izberi vse",
|
"action.select-all": "Izberi vse",
|
||||||
"action.select-none": "Počisti izbiro",
|
"action.select-none": "Počisti izbiro",
|
||||||
"action.send-backward": "Pošlji nazaj",
|
"action.send-backward": "Pošlji nazaj",
|
||||||
"action.send-to-back": "Pošlji v ozadje",
|
"action.send-to-back": "Pošlji v ozadje",
|
||||||
"action.share-project": "Deli ta projekt",
|
"action.share-project": "Deli ta projekt",
|
||||||
"action.stack-horizontal": "Naloži vodoravno",
|
"action.stack-horizontal": "Naloži vodoravno",
|
||||||
"action.stack-vertical": "Naloži navpično",
|
"action.stack-vertical": "Naloži navpično",
|
||||||
"action.stack-horizontal.short": "Naloži vodoravno",
|
"action.stack-horizontal.short": "Naloži vodoravno",
|
||||||
"action.stack-vertical.short": "Naloži navpično",
|
"action.stack-vertical.short": "Naloži navpično",
|
||||||
"action.stretch-horizontal": "Raztegnite vodoravno",
|
"action.stretch-horizontal": "Raztegnite vodoravno",
|
||||||
"action.stretch-vertical": "Raztegni navpično",
|
"action.stretch-vertical": "Raztegni navpično",
|
||||||
"action.stretch-horizontal.short": "Raztezanje vodoravno",
|
"action.stretch-horizontal.short": "Raztezanje vodoravno",
|
||||||
"action.stretch-vertical.short": "Raztezanje navpično",
|
"action.stretch-vertical.short": "Raztezanje navpično",
|
||||||
"action.toggle-auto-size": "Preklopi samodejno velikost",
|
"action.toggle-auto-size": "Preklopi samodejno velikost",
|
||||||
"action.toggle-dark-mode.menu": "Temni način",
|
"action.toggle-dark-mode.menu": "Temni način",
|
||||||
"action.toggle-dark-mode": "Preklopi temni način",
|
"action.toggle-dark-mode": "Preklopi temni način",
|
||||||
"action.toggle-debug-mode.menu": "Način odpravljanja napak",
|
"action.toggle-debug-mode.menu": "Način odpravljanja napak",
|
||||||
"action.toggle-debug-mode": "Preklopi način odpravljanja napak",
|
"action.toggle-debug-mode": "Preklopi način odpravljanja napak",
|
||||||
"action.toggle-focus-mode.menu": "Osredotočen način",
|
"action.toggle-focus-mode.menu": "Osredotočen način",
|
||||||
"action.toggle-focus-mode": "Preklopi na osredotočen način",
|
"action.toggle-focus-mode": "Preklopi na osredotočen način",
|
||||||
"action.toggle-grid.menu": "Prikaži mrežo",
|
"action.toggle-grid.menu": "Prikaži mrežo",
|
||||||
"action.toggle-grid": "Preklopi mrežo",
|
"action.toggle-grid": "Preklopi mrežo",
|
||||||
"action.toggle-snap-mode.menu": "Vedno pripni",
|
"action.toggle-snap-mode.menu": "Vedno pripni",
|
||||||
"action.toggle-snap-mode": "Preklopi pripenjanje",
|
"action.toggle-snap-mode": "Preklopi pripenjanje",
|
||||||
"action.toggle-tool-lock.menu": "Zaklepanje orodja",
|
"action.toggle-tool-lock.menu": "Zaklepanje orodja",
|
||||||
"action.toggle-tool-lock": "Preklopi zaklepanje orodja",
|
"action.toggle-tool-lock": "Preklopi zaklepanje orodja",
|
||||||
"action.toggle-transparent.context-menu": "Prozorno",
|
"action.toggle-transparent.context-menu": "Prozorno",
|
||||||
"action.toggle-transparent.menu": "Prozorno",
|
"action.toggle-transparent.menu": "Prozorno",
|
||||||
"action.toggle-transparent": "Preklopi prosojno ozadje",
|
"action.toggle-transparent": "Preklopi prosojno ozadje",
|
||||||
"action.undo": "Razveljavi",
|
"action.undo": "Razveljavi",
|
||||||
"action.ungroup": "Razdruži",
|
"action.ungroup": "Razdruži",
|
||||||
"action.zoom-in": "Povečaj",
|
"action.zoom-in": "Povečaj",
|
||||||
"action.zoom-out": "Pomanjšaj",
|
"action.zoom-out": "Pomanjšaj",
|
||||||
"action.zoom-to-100": "Povečaj na 100 %",
|
"action.zoom-to-100": "Povečaj na 100 %",
|
||||||
"action.zoom-to-fit": "Povečaj do prileganja",
|
"action.zoom-to-fit": "Povečaj do prileganja",
|
||||||
"action.zoom-to-selection": "Pomakni na izbiro",
|
"action.zoom-to-selection": "Pomakni na izbiro",
|
||||||
"color-style.black": "Črna",
|
"color-style.black": "Črna",
|
||||||
"color-style.blue": "Modra",
|
"color-style.blue": "Modra",
|
||||||
"color-style.green": "Zelena",
|
"color-style.green": "Zelena",
|
||||||
"color-style.grey": "Siva",
|
"color-style.grey": "Siva",
|
||||||
"color-style.light-blue": "Svetlo modra",
|
"color-style.light-blue": "Svetlo modra",
|
||||||
"color-style.light-green": "Svetlo zelena",
|
"color-style.light-green": "Svetlo zelena",
|
||||||
"color-style.light-red": "Svetlo rdeča",
|
"color-style.light-red": "Svetlo rdeča",
|
||||||
"color-style.light-violet": "Svetlo vijolična",
|
"color-style.light-violet": "Svetlo vijolična",
|
||||||
"color-style.orange": "Oranžna",
|
"color-style.orange": "Oranžna",
|
||||||
"color-style.red": "Rdeča",
|
"color-style.red": "Rdeča",
|
||||||
"color-style.violet": "Vijolična",
|
"color-style.violet": "Vijolična",
|
||||||
"color-style.yellow": "Rumena",
|
"color-style.yellow": "Rumena",
|
||||||
"fill-style.none": "Brez",
|
"fill-style.none": "Brez",
|
||||||
"fill-style.semi": "Polovično",
|
"fill-style.semi": "Polovično",
|
||||||
"fill-style.solid": "Polno",
|
"fill-style.solid": "Polno",
|
||||||
"fill-style.pattern": "Vzorec",
|
"fill-style.pattern": "Vzorec",
|
||||||
"dash-style.dashed": "Črtkano",
|
"dash-style.dashed": "Črtkano",
|
||||||
"dash-style.dotted": "Pikčasto",
|
"dash-style.dotted": "Pikčasto",
|
||||||
"dash-style.draw": "Narisano",
|
"dash-style.draw": "Narisano",
|
||||||
"dash-style.solid": "Polno",
|
"dash-style.solid": "Polno",
|
||||||
"size-style.s": "Malo",
|
"size-style.s": "Malo",
|
||||||
"size-style.m": "Srednje",
|
"size-style.m": "Srednje",
|
||||||
"size-style.l": "Veliko",
|
"size-style.l": "Veliko",
|
||||||
"size-style.xl": "Zelo veliko",
|
"size-style.xl": "Zelo veliko",
|
||||||
"opacity-style.0.1": "10 %",
|
"opacity-style.0.1": "10 %",
|
||||||
"opacity-style.0.25": "25 %",
|
"opacity-style.0.25": "25 %",
|
||||||
"opacity-style.0.5": "50 %",
|
"opacity-style.0.5": "50 %",
|
||||||
"opacity-style.0.75": "75 %",
|
"opacity-style.0.75": "75 %",
|
||||||
"opacity-style.1": "100 %",
|
"opacity-style.1": "100 %",
|
||||||
"font-style.draw": "Draw",
|
"font-style.draw": "Draw",
|
||||||
"font-style.sans": "Sans",
|
"font-style.sans": "Sans",
|
||||||
"font-style.serif": "Serif",
|
"font-style.serif": "Serif",
|
||||||
"font-style.mono": "Mono",
|
"font-style.mono": "Mono",
|
||||||
"align-style.start": "Začetek",
|
"align-style.start": "Začetek",
|
||||||
"align-style.middle": "Sredina",
|
"align-style.middle": "Sredina",
|
||||||
"align-style.end": "Konec",
|
"align-style.end": "Konec",
|
||||||
"align-style.justify": "Poravnaj",
|
"align-style.justify": "Poravnaj",
|
||||||
"geo-style.arrow-down": "Puščica navzdol",
|
"geo-style.arrow-down": "Puščica navzdol",
|
||||||
"geo-style.arrow-left": "Puščica levo",
|
"geo-style.arrow-left": "Puščica levo",
|
||||||
"geo-style.arrow-right": "Puščica desno",
|
"geo-style.arrow-right": "Puščica desno",
|
||||||
"geo-style.arrow-up": "Puščica navzgor",
|
"geo-style.arrow-up": "Puščica navzgor",
|
||||||
"geo-style.diamond": "Diamant",
|
"geo-style.diamond": "Diamant",
|
||||||
"geo-style.ellipse": "Elipsa",
|
"geo-style.ellipse": "Elipsa",
|
||||||
"geo-style.hexagon": "Šesterokotnik",
|
"geo-style.hexagon": "Šesterokotnik",
|
||||||
"geo-style.octagon": "Osmerokotnik",
|
"geo-style.octagon": "Osmerokotnik",
|
||||||
"geo-style.oval": "Oval",
|
"geo-style.oval": "Oval",
|
||||||
"geo-style.pentagon": "Peterokotnik",
|
"geo-style.pentagon": "Peterokotnik",
|
||||||
"geo-style.rectangle": "Pravokotnik",
|
"geo-style.rectangle": "Pravokotnik",
|
||||||
"geo-style.rhombus-2": "Romb 2",
|
"geo-style.rhombus-2": "Romb 2",
|
||||||
"geo-style.rhombus": "Romb",
|
"geo-style.rhombus": "Romb",
|
||||||
"geo-style.star": "Zvezda",
|
"geo-style.star": "Zvezda",
|
||||||
"geo-style.trapezoid": "Trapez",
|
"geo-style.trapezoid": "Trapez",
|
||||||
"geo-style.triangle": "Trikotnik",
|
"geo-style.triangle": "Trikotnik",
|
||||||
"geo-style.x-box": "X polje",
|
"geo-style.x-box": "X polje",
|
||||||
"arrowheadStart-style.none": "Brez",
|
"arrowheadStart-style.none": "Brez",
|
||||||
"arrowheadStart-style.arrow": "Puščica",
|
"arrowheadStart-style.arrow": "Puščica",
|
||||||
"arrowheadStart-style.bar": "Črta",
|
"arrowheadStart-style.bar": "Črta",
|
||||||
"arrowheadStart-style.diamond": "Diamant",
|
"arrowheadStart-style.diamond": "Diamant",
|
||||||
"arrowheadStart-style.dot": "Pika",
|
"arrowheadStart-style.dot": "Pika",
|
||||||
"arrowheadStart-style.inverted": "Obrnjeno",
|
"arrowheadStart-style.inverted": "Obrnjeno",
|
||||||
"arrowheadStart-style.pipe": "Cev",
|
"arrowheadStart-style.pipe": "Cev",
|
||||||
"arrowheadStart-style.square": "Kvadrat",
|
"arrowheadStart-style.square": "Kvadrat",
|
||||||
"arrowheadStart-style.triangle": "Trikotnik",
|
"arrowheadStart-style.triangle": "Trikotnik",
|
||||||
"arrowheadEnd-style.none": "Brez",
|
"arrowheadEnd-style.none": "Brez",
|
||||||
"arrowheadEnd-style.arrow": "Puščica",
|
"arrowheadEnd-style.arrow": "Puščica",
|
||||||
"arrowheadEnd-style.bar": "Črta",
|
"arrowheadEnd-style.bar": "Črta",
|
||||||
"arrowheadEnd-style.diamond": "Diamant",
|
"arrowheadEnd-style.diamond": "Diamant",
|
||||||
"arrowheadEnd-style.dot": "Pika",
|
"arrowheadEnd-style.dot": "Pika",
|
||||||
"arrowheadEnd-style.inverted": "Obrnjeno",
|
"arrowheadEnd-style.inverted": "Obrnjeno",
|
||||||
"arrowheadEnd-style.pipe": "Cev",
|
"arrowheadEnd-style.pipe": "Cev",
|
||||||
"arrowheadEnd-style.square": "Kvadrat",
|
"arrowheadEnd-style.square": "Kvadrat",
|
||||||
"arrowheadEnd-style.triangle": "Trikotnik",
|
"arrowheadEnd-style.triangle": "Trikotnik",
|
||||||
"spline-style.line": "Črta",
|
"spline-style.line": "Črta",
|
||||||
"spline-style.cubic": "Kubično",
|
"spline-style.cubic": "Kubično",
|
||||||
"tool.select": "Izbor",
|
"tool.select": "Izbor",
|
||||||
"tool.hand": "Roka",
|
"tool.hand": "Roka",
|
||||||
"tool.draw": "Risanje",
|
"tool.draw": "Risanje",
|
||||||
"tool.eraser": "Radirka",
|
"tool.eraser": "Radirka",
|
||||||
"tool.arrow-down": "Puščica navzdol",
|
"tool.arrow-down": "Puščica navzdol",
|
||||||
"tool.arrow-left": "Puščica levo",
|
"tool.arrow-left": "Puščica levo",
|
||||||
"tool.arrow-right": "Puščica desno",
|
"tool.arrow-right": "Puščica desno",
|
||||||
"tool.arrow-up": "Puščica navzgor",
|
"tool.arrow-up": "Puščica navzgor",
|
||||||
"tool.arrow": "Puščica",
|
"tool.arrow": "Puščica",
|
||||||
"tool.diamond": "Diamant",
|
"tool.diamond": "Diamant",
|
||||||
"tool.ellipse": "Elipsa",
|
"tool.ellipse": "Elipsa",
|
||||||
"tool.hexagon": "Šesterokotnik",
|
"tool.hexagon": "Šesterokotnik",
|
||||||
"tool.line": "Črta",
|
"tool.line": "Črta",
|
||||||
"tool.octagon": "Osmerokotnik",
|
"tool.octagon": "Osmerokotnik",
|
||||||
"tool.oval": "Oval",
|
"tool.oval": "Oval",
|
||||||
"tool.pentagon": "Peterokotnik",
|
"tool.pentagon": "Peterokotnik",
|
||||||
"tool.rectangle": "Pravokotnik",
|
"tool.rectangle": "Pravokotnik",
|
||||||
"tool.rhombus": "Romb",
|
"tool.rhombus": "Romb",
|
||||||
"tool.star": "Zvezda",
|
"tool.star": "Zvezda",
|
||||||
"tool.trapezoid": "Trapez",
|
"tool.trapezoid": "Trapez",
|
||||||
"tool.triangle": "Trikotnik",
|
"tool.triangle": "Trikotnik",
|
||||||
"tool.x-box": "X polje",
|
"tool.x-box": "X polje",
|
||||||
"tool.asset": "Sredstvo",
|
"tool.asset": "Sredstvo",
|
||||||
"tool.frame": "Okvir",
|
"tool.frame": "Okvir",
|
||||||
"tool.note": "Opomba",
|
"tool.note": "Opomba",
|
||||||
"tool.embed": "Vdelava",
|
"tool.embed": "Vdelava",
|
||||||
"tool.text": "Besedilo",
|
"tool.text": "Besedilo",
|
||||||
"menu.title": "Meni",
|
"menu.title": "Meni",
|
||||||
"menu.copy-as": "Kopiraj kot",
|
"menu.copy-as": "Kopiraj kot",
|
||||||
"menu.edit": "Uredi",
|
"menu.edit": "Uredi",
|
||||||
"menu.export-as": "Izvozi kot",
|
"menu.export-as": "Izvozi kot",
|
||||||
"menu.file": "Datoteka",
|
"menu.file": "Datoteka",
|
||||||
"menu.language": "Jezik",
|
"menu.language": "Jezik",
|
||||||
"menu.preferences": "Nastavitve",
|
"menu.preferences": "Nastavitve",
|
||||||
"menu.view": "Pogled",
|
"menu.view": "Pogled",
|
||||||
"context-menu.arrange": "Preuredi",
|
"context-menu.arrange": "Preuredi",
|
||||||
"context-menu.copy-as": "Kopiraj kot",
|
"context-menu.copy-as": "Kopiraj kot",
|
||||||
"context-menu.export-as": "Izvozi kot",
|
"context-menu.export-as": "Izvozi kot",
|
||||||
"context-menu.move-to-page": "Premakni na stran",
|
"context-menu.move-to-page": "Premakni na stran",
|
||||||
"context-menu.reorder": "Preuredite",
|
"context-menu.reorder": "Preuredite",
|
||||||
"page-menu.title": "Strani",
|
"page-menu.title": "Strani",
|
||||||
"page-menu.create-new-page": "Ustvari novo stran",
|
"page-menu.create-new-page": "Ustvari novo stran",
|
||||||
"page-menu.max-page-count-reached": "Doseženo največje število strani",
|
"page-menu.max-page-count-reached": "Doseženo največje število strani",
|
||||||
"page-menu.new-page-initial-name": "Stran 1",
|
"page-menu.new-page-initial-name": "Stran 1",
|
||||||
"page-menu.edit-start": "Uredi",
|
"page-menu.edit-start": "Uredi",
|
||||||
"page-menu.edit-done": "Zaključi",
|
"page-menu.edit-done": "Zaključi",
|
||||||
"page-menu.submenu.rename": "Preimenuj",
|
"page-menu.submenu.rename": "Preimenuj",
|
||||||
"page-menu.submenu.duplicate-page": "Podvoji",
|
"page-menu.submenu.duplicate-page": "Podvoji",
|
||||||
"page-menu.submenu.title": "Meni",
|
"page-menu.submenu.title": "Meni",
|
||||||
"page-menu.submenu.move-down": "Premakni navzdol",
|
"page-menu.submenu.move-down": "Premakni navzdol",
|
||||||
"page-menu.submenu.move-up": "Premakni navzgor",
|
"page-menu.submenu.move-up": "Premakni navzgor",
|
||||||
"page-menu.submenu.delete": "Izbriši",
|
"page-menu.submenu.delete": "Izbriši",
|
||||||
"share-menu.title": "Deli",
|
"share-menu.title": "Deli",
|
||||||
"share-menu.share-project": "Deli ta projekt",
|
"share-menu.share-project": "Deli ta projekt",
|
||||||
"share-menu.copy-link": "Kopiraj povezavo",
|
"share-menu.copy-link": "Kopiraj povezavo",
|
||||||
"share-menu.readonly-link": "Samo za branje",
|
"share-menu.readonly-link": "Samo za branje",
|
||||||
"share-menu.copy-readonly-link": "Kopiraj povezavo samo za branje",
|
"share-menu.copy-readonly-link": "Kopiraj povezavo samo za branje",
|
||||||
"share-menu.offline-note": "Skupna raba tega projekta bo ustvarila živo kopijo na novem URL-ju. URL lahko delite z do tridesetimi drugimi osebami, s katerimi lahko skupaj gledate in urejate vsebino.",
|
"share-menu.offline-note": "Skupna raba tega projekta bo ustvarila živo kopijo na novem URL-ju. URL lahko delite z do tridesetimi drugimi osebami, s katerimi lahko skupaj gledate in urejate vsebino.",
|
||||||
"share-menu.copy-link-note": "Vsakdo s povezavo si bo lahko ogledal in urejal ta projekt.",
|
"share-menu.copy-link-note": "Vsakdo s povezavo si bo lahko ogledal in urejal ta projekt.",
|
||||||
"share-menu.copy-readonly-link-note": "Vsakdo s povezavo si bo lahko ogledal (vendar ne urejal) ta projekt.",
|
"share-menu.copy-readonly-link-note": "Vsakdo s povezavo si bo lahko ogledal (vendar ne urejal) ta projekt.",
|
||||||
"share-menu.project-too-large": "Žal tega projekta ni mogoče deliti, ker je prevelik. Delamo na tem!",
|
"share-menu.project-too-large": "Žal tega projekta ni mogoče deliti, ker je prevelik. Delamo na tem!",
|
||||||
"people-menu.title": "Ljudje",
|
"people-menu.title": "Ljudje",
|
||||||
"people-menu.change-name": "Spremeni ime",
|
"people-menu.change-name": "Spremeni ime",
|
||||||
"people-menu.change-color": "Spremeni barvo",
|
"people-menu.change-color": "Spremeni barvo",
|
||||||
"people-menu.user": "(Ti)",
|
"people-menu.user": "(Ti)",
|
||||||
"people-menu.invite": "Povabi ostale",
|
"people-menu.invite": "Povabi ostale",
|
||||||
"help-menu.title": "Pomoč in viri",
|
"help-menu.title": "Pomoč in viri",
|
||||||
"help-menu.about": "O nas",
|
"help-menu.about": "O nas",
|
||||||
"help-menu.discord": "Discord",
|
"help-menu.discord": "Discord",
|
||||||
"help-menu.github": "GitHub",
|
"help-menu.github": "GitHub",
|
||||||
"help-menu.keyboard-shortcuts": "Bližnjice na tipkovnici",
|
"help-menu.keyboard-shortcuts": "Bližnjice na tipkovnici",
|
||||||
"help-menu.twitter": "Twitter",
|
"help-menu.twitter": "Twitter",
|
||||||
"actions-menu.title": "Akcije",
|
"actions-menu.title": "Akcije",
|
||||||
"edit-link-dialog.title": "Uredi povezavo",
|
"edit-link-dialog.title": "Uredi povezavo",
|
||||||
"edit-link-dialog.invalid-url": "Povezava mora biti veljavna",
|
"edit-link-dialog.invalid-url": "Povezava mora biti veljavna",
|
||||||
"edit-link-dialog.detail": "Povezave se bodo odprle v novem zavihku.",
|
"edit-link-dialog.detail": "Povezave se bodo odprle v novem zavihku.",
|
||||||
"edit-link-dialog.url": "URL",
|
"edit-link-dialog.url": "URL",
|
||||||
"edit-link-dialog.clear": "Počisti",
|
"edit-link-dialog.clear": "Počisti",
|
||||||
"edit-link-dialog.save": "Nadaljuj",
|
"edit-link-dialog.save": "Nadaljuj",
|
||||||
"edit-link-dialog.cancel": "Prekliči",
|
"edit-link-dialog.cancel": "Prekliči",
|
||||||
"embed-dialog.title": "Ustvari vdelavo",
|
"embed-dialog.title": "Ustvari vdelavo",
|
||||||
"embed-dialog.back": "Nazaj",
|
"embed-dialog.back": "Nazaj",
|
||||||
"embed-dialog.create": "Ustvari",
|
"embed-dialog.create": "Ustvari",
|
||||||
"embed-dialog.cancel": "Prekliči",
|
"embed-dialog.cancel": "Prekliči",
|
||||||
"embed-dialog.url": "URL",
|
"embed-dialog.url": "URL",
|
||||||
"embed-dialog.instruction": "Prilepite URL spletnega mesta, da ustvarite vdelavo.",
|
"embed-dialog.instruction": "Prilepite URL spletnega mesta, da ustvarite vdelavo.",
|
||||||
"embed-dialog.invalid-url": "Iz tega URL-ja nismo mogli ustvariti vdelave.",
|
"embed-dialog.invalid-url": "Iz tega URL-ja nismo mogli ustvariti vdelave.",
|
||||||
"edit-pages-dialog.move-down": "Premakni navzdol",
|
"edit-pages-dialog.move-down": "Premakni navzdol",
|
||||||
"edit-pages-dialog.move-up": "Premakni navzgor",
|
"edit-pages-dialog.move-up": "Premakni navzgor",
|
||||||
"shortcuts-dialog.title": "Bližnjice na tipkovnici",
|
"shortcuts-dialog.title": "Bližnjice na tipkovnici",
|
||||||
"shortcuts-dialog.edit": "Uredi",
|
"shortcuts-dialog.edit": "Uredi",
|
||||||
"shortcuts-dialog.file": "Datoteka",
|
"shortcuts-dialog.file": "Datoteka",
|
||||||
"shortcuts-dialog.preferences": "Nastavitve",
|
"shortcuts-dialog.preferences": "Nastavitve",
|
||||||
"shortcuts-dialog.tools": "Orodja",
|
"shortcuts-dialog.tools": "Orodja",
|
||||||
"shortcuts-dialog.transform": "Preoblikuj",
|
"shortcuts-dialog.transform": "Preoblikuj",
|
||||||
"shortcuts-dialog.view": "Pogled",
|
"shortcuts-dialog.view": "Pogled",
|
||||||
"style-panel.title": "Stili",
|
"style-panel.title": "Stili",
|
||||||
"style-panel.align": "Poravnava",
|
"style-panel.align": "Poravnava",
|
||||||
"style-panel.arrowheads": "Puščice",
|
"style-panel.arrowheads": "Puščice",
|
||||||
"style-panel.color": "Barva",
|
"style-panel.color": "Barva",
|
||||||
"style-panel.dash": "Črtasto",
|
"style-panel.dash": "Črtasto",
|
||||||
"style-panel.fill": "Polnilo",
|
"style-panel.fill": "Polnilo",
|
||||||
"style-panel.font": "Pisava",
|
"style-panel.font": "Pisava",
|
||||||
"style-panel.geo": "Oblika",
|
"style-panel.geo": "Oblika",
|
||||||
"style-panel.mixed": "Mešano",
|
"style-panel.mixed": "Mešano",
|
||||||
"style-panel.opacity": "Motnost",
|
"style-panel.opacity": "Motnost",
|
||||||
"style-panel.size": "Velikost",
|
"style-panel.size": "Velikost",
|
||||||
"style-panel.spline": "Krivulja",
|
"style-panel.spline": "Krivulja",
|
||||||
"tool-panel.drawing": "Risanje",
|
"tool-panel.drawing": "Risanje",
|
||||||
"tool-panel.shapes": "Oblike",
|
"tool-panel.shapes": "Oblike",
|
||||||
"navigation-zone.toggle-minimap": "Preklopi mini zemljevid",
|
"navigation-zone.toggle-minimap": "Preklopi mini zemljevid",
|
||||||
"navigation-zone.zoom": "Povečava",
|
"navigation-zone.zoom": "Povečava",
|
||||||
"focus-mode.toggle-focus-mode": "Preklopi na osredotočen način",
|
"focus-mode.toggle-focus-mode": "Preklopi na osredotočen način",
|
||||||
"toast.close": "Zapri",
|
"toast.close": "Zapri",
|
||||||
"file-system.file-open-error.title": "Datoteke ni bilo mogoče odpreti",
|
"file-system.file-open-error.title": "Datoteke ni bilo mogoče odpreti",
|
||||||
"file-system.file-open-error.not-a-tldraw-file": "Datoteka, ki ste jo poskušali odpreti, ni videti kot datoteka tldraw.",
|
"file-system.file-open-error.not-a-tldraw-file": "Datoteka, ki ste jo poskušali odpreti, ni videti kot datoteka tldraw.",
|
||||||
"file-system.file-open-error.file-format-version-too-new": "Datoteka, ki ste jo poskušali odpreti, je iz novejše različice tldraw. Ponovno naložite stran in poskusite znova.",
|
"file-system.file-open-error.file-format-version-too-new": "Datoteka, ki ste jo poskušali odpreti, je iz novejše različice tldraw. Ponovno naložite stran in poskusite znova.",
|
||||||
"file-system.file-open-error.generic-corrupted-file": "Datoteka, ki ste jo poskušali odpreti, je poškodovana.",
|
"file-system.file-open-error.generic-corrupted-file": "Datoteka, ki ste jo poskušali odpreti, je poškodovana.",
|
||||||
"file-system.confirm-open.title": "Prepiši trenutni projekt?",
|
"file-system.confirm-open.title": "Prepiši trenutni projekt?",
|
||||||
"file-system.confirm-open.description": "Če ustvarite nov projekt, boste izbrisali trenutni projekt in vse neshranjene spremembe bodo izgubljene. Ste prepričani, da želite nadaljevati?",
|
"file-system.confirm-open.description": "Če ustvarite nov projekt, boste izbrisali trenutni projekt in vse neshranjene spremembe bodo izgubljene. Ste prepričani, da želite nadaljevati?",
|
||||||
"file-system.confirm-open.cancel": "Prekliči",
|
"file-system.confirm-open.cancel": "Prekliči",
|
||||||
"file-system.confirm-open.open": "Odpri datoteko",
|
"file-system.confirm-open.open": "Odpri datoteko",
|
||||||
"file-system.confirm-open.dont-show-again": "Ne sprašuj znova",
|
"file-system.confirm-open.dont-show-again": "Ne sprašuj znova",
|
||||||
"toast.error.export-fail.title": "Izvoz ni uspel",
|
"toast.error.export-fail.title": "Izvoz ni uspel",
|
||||||
"toast.error.export-fail.desc": "Izvoz slike ni uspel",
|
"toast.error.export-fail.desc": "Izvoz slike ni uspel",
|
||||||
"toast.error.copy-fail.title": "Kopiranje ni uspelo",
|
"toast.error.copy-fail.title": "Kopiranje ni uspelo",
|
||||||
"toast.error.copy-fail.desc": "Kopiranje slike ni uspelo",
|
"toast.error.copy-fail.desc": "Kopiranje slike ni uspelo",
|
||||||
"file-system.shared-document-file-open-error.title": "Datoteke ni bilo mogoče odpreti",
|
"file-system.shared-document-file-open-error.title": "Datoteke ni bilo mogoče odpreti",
|
||||||
"file-system.shared-document-file-open-error.description": "Odpiranje datotek v skupnih projektih ni podprto.",
|
"file-system.shared-document-file-open-error.description": "Odpiranje datotek v skupnih projektih ni podprto.",
|
||||||
"vscode.file-open.dont-show-again": "Ne sprašuj znova",
|
"vscode.file-open.dont-show-again": "Ne sprašuj znova",
|
||||||
"vscode.file-open.desc": "Ta datoteka je bila ustvarjena s starejšo različico tldraw. Ali jo želite posodobiti, da bo deloval z novo različico?",
|
"vscode.file-open.desc": "Ta datoteka je bila ustvarjena s starejšo različico tldraw. Ali jo želite posodobiti, da bo deloval z novo različico?",
|
||||||
"context.pages.new-page": "Nova stran",
|
"context.pages.new-page": "Nova stran",
|
||||||
"style-panel.arrowhead-start": "Začetek",
|
"style-panel.arrowhead-start": "Začetek",
|
||||||
"style-panel.arrowhead-end": "Konec",
|
"style-panel.arrowhead-end": "Konec",
|
||||||
"vscode.file-open.open": "Nadaljuj",
|
"vscode.file-open.open": "Nadaljuj",
|
||||||
"vscode.file-open.backup": "Varnostna kopija",
|
"vscode.file-open.backup": "Varnostna kopija",
|
||||||
"vscode.file-open.backup-saved": "Varnostna kopija shranjena",
|
"vscode.file-open.backup-saved": "Varnostna kopija shranjena",
|
||||||
"vscode.file-open.backup-failed": "Varnostno kopiranje ni uspelo: to ni datoteka .tldr.",
|
"vscode.file-open.backup-failed": "Varnostno kopiranje ni uspelo: to ni datoteka .tldr.",
|
||||||
"tool-panel.more": "Več",
|
"tool-panel.more": "Več",
|
||||||
"debug-panel.more": "Več",
|
"debug-panel.more": "Več",
|
||||||
"action.new-project": "Nov projekt",
|
"action.new-project": "Nov projekt",
|
||||||
"file-system.confirm-clear.title": "Počisti trenutni projekt?",
|
"file-system.confirm-clear.title": "Počisti trenutni projekt?",
|
||||||
"file-system.confirm-clear.description": "Če ustvarite nov projekt, boste izbrisali trenutni projekt in vse neshranjene spremembe bodo izgubljene. Ste prepričani, da želite nadaljevati?",
|
"file-system.confirm-clear.description": "Če ustvarite nov projekt, boste izbrisali trenutni projekt in vse neshranjene spremembe bodo izgubljene. Ste prepričani, da želite nadaljevati?",
|
||||||
"file-system.confirm-clear.cancel": "Prekliči",
|
"file-system.confirm-clear.cancel": "Prekliči",
|
||||||
"file-system.confirm-clear.continue": "Nadaljuj",
|
"file-system.confirm-clear.continue": "Nadaljuj",
|
||||||
"file-system.confirm-clear.dont-show-again": "Ne sprašuj znova",
|
"file-system.confirm-clear.dont-show-again": "Ne sprašuj znova",
|
||||||
"action.stop-following": "Prenehaj slediti",
|
"action.stop-following": "Prenehaj slediti",
|
||||||
"people-menu.follow": "Sledi",
|
"people-menu.follow": "Sledi",
|
||||||
"style-panel.position": "Položaj",
|
"style-panel.position": "Položaj",
|
||||||
"page-menu.go-to-page": "Pojdi na stran",
|
"page-menu.go-to-page": "Pojdi na stran",
|
||||||
"action.insert-embed": "Vstavi vdelavo",
|
"action.insert-embed": "Vstavi vdelavo",
|
||||||
"people-menu.following": "Sledim",
|
"people-menu.following": "Sledim",
|
||||||
"people-menu.leading": "Sledi vam",
|
"people-menu.leading": "Sledi vam",
|
||||||
"geo-style.check-box": "Potrditveno polje",
|
"geo-style.check-box": "Potrditveno polje",
|
||||||
"tool.check-box": "Potrditveno polje",
|
"tool.check-box": "Potrditveno polje",
|
||||||
"share-menu.create-snapshot-link": "Ustvari povezavo do posnetka",
|
"share-menu.create-snapshot-link": "Ustvari povezavo do posnetka",
|
||||||
"share-menu.save-note": "Ta projekt prenesite na svoj računalnik kot datoteko .tldr.",
|
"share-menu.save-note": "Ta projekt prenesite na svoj računalnik kot datoteko .tldr.",
|
||||||
"share-menu.fork-note": "Na podlagi tega posnetka ustvarite nov skupni projekt.",
|
"share-menu.fork-note": "Na podlagi tega posnetka ustvarite nov skupni projekt.",
|
||||||
"share-menu.snapshot-link-note": "Zajemite in delite ta projekt kot povezavo do posnetka samo za branje.",
|
"share-menu.snapshot-link-note": "Zajemite in delite ta projekt kot povezavo do posnetka samo za branje.",
|
||||||
"share-menu.upload-failed": "Oprostite, trenutno nismo mogli naložiti vašega projekta. Poskusite znova ali nam sporočite, če se težava ponovi.",
|
"share-menu.upload-failed": "Oprostite, trenutno nismo mogli naložiti vašega projekta. Poskusite znova ali nam sporočite, če se težava ponovi.",
|
||||||
"style-panel.vertical-align": "Navpična poravnava",
|
"style-panel.vertical-align": "Navpična poravnava",
|
||||||
"tool.laser": "Laser",
|
"tool.laser": "Laser",
|
||||||
"action.fork-project": "Naredi kopijo projekta",
|
"action.fork-project": "Naredi kopijo projekta",
|
||||||
"action.leave-shared-project": "Zapusti skupni projekt",
|
"action.leave-shared-project": "Zapusti skupni projekt",
|
||||||
"sharing.confirm-leave.title": "Zapusti trenutni projekt?",
|
"sharing.confirm-leave.title": "Zapusti trenutni projekt?",
|
||||||
"sharing.confirm-leave.description": "Ali ste prepričani, da želite zapustiti ta skupni projekt? Nanj se lahko vrnete tako, da se ponovno vrnete na njegov URL.",
|
"sharing.confirm-leave.description": "Ali ste prepričani, da želite zapustiti ta skupni projekt? Nanj se lahko vrnete tako, da se ponovno vrnete na njegov URL.",
|
||||||
"sharing.confirm-leave.cancel": "Prekliči",
|
"sharing.confirm-leave.cancel": "Prekliči",
|
||||||
"sharing.confirm-leave.leave": "Zapusti",
|
"sharing.confirm-leave.leave": "Zapusti",
|
||||||
"sharing.confirm-leave.dont-show-again": "Ne sprašuj znova",
|
"sharing.confirm-leave.dont-show-again": "Ne sprašuj znova",
|
||||||
"action.toggle-reduce-motion.menu": "Zmanjšaj gibanje",
|
"action.toggle-reduce-motion.menu": "Zmanjšaj gibanje",
|
||||||
"action.toggle-reduce-motion": "Preklop zmanjšanja gibanja",
|
"action.toggle-reduce-motion": "Preklop zmanjšanja gibanja",
|
||||||
"tool.highlight": "Marker",
|
"tool.highlight": "Marker",
|
||||||
"action.toggle-lock": "Zakleni \/ odkleni",
|
"action.toggle-lock": "Zakleni / odkleni",
|
||||||
"share-menu.default-project-name": "Skupni projekt",
|
"share-menu.default-project-name": "Skupni projekt",
|
||||||
"home-project-dialog.title": "Lokalni projekt",
|
"home-project-dialog.title": "Lokalni projekt",
|
||||||
"home-project-dialog.description": "To je vaš lokalni projekt. Namenjen je samo vam!",
|
"home-project-dialog.description": "To je vaš lokalni projekt. Namenjen je samo vam!",
|
||||||
"rename-project-dialog.title": "Preimenuj projekt",
|
"rename-project-dialog.title": "Preimenuj projekt",
|
||||||
"rename-project-dialog.cancel": "Prekliči",
|
"rename-project-dialog.cancel": "Prekliči",
|
||||||
"rename-project-dialog.rename": "Preimenuj",
|
"rename-project-dialog.rename": "Preimenuj",
|
||||||
"home-project-dialog.ok": "V redu",
|
"home-project-dialog.ok": "V redu",
|
||||||
"action.open-cursor-chat": "Klepet s kazalcem",
|
"action.open-cursor-chat": "Klepet s kazalcem",
|
||||||
"shortcuts-dialog.collaboration": "Sodelovanje",
|
"shortcuts-dialog.collaboration": "Sodelovanje",
|
||||||
"cursor-chat.type-to-chat": "Vnesite za klepet ...",
|
"cursor-chat.type-to-chat": "Vnesite za klepet ...",
|
||||||
"geo-style.cloud": "Oblak",
|
"geo-style.cloud": "Oblak",
|
||||||
"tool.cloud": "Oblak",
|
"tool.cloud": "Oblak",
|
||||||
"action.unlock-all": "Odkleni vse",
|
"action.unlock-all": "Odkleni vse",
|
||||||
"status.offline": "Brez povezave",
|
"status.offline": "Brez povezave",
|
||||||
"status.online": "Povezan",
|
"status.online": "Povezan",
|
||||||
"action.remove-frame": "Odstrani okvir",
|
"action.remove-frame": "Odstrani okvir",
|
||||||
"action.fit-frame-to-content": "Prilagodi vsebini",
|
"action.fit-frame-to-content": "Prilagodi vsebini",
|
||||||
"action.toggle-edge-scrolling.menu": "Pomikanje ob robovih",
|
"action.toggle-edge-scrolling.menu": "Pomikanje ob robovih",
|
||||||
"action.toggle-edge-scrolling": "Preklopi pomikanje ob robovih",
|
"action.toggle-edge-scrolling": "Preklopi pomikanje ob robovih",
|
||||||
"verticalAlign-style.start": "Vrh",
|
"verticalAlign-style.start": "Vrh",
|
||||||
"verticalAlign-style.middle": "Sredina",
|
"verticalAlign-style.middle": "Sredina",
|
||||||
"verticalAlign-style.end": "Dno"
|
"verticalAlign-style.end": "Dno"
|
||||||
}
|
}
|
|
@ -16,11 +16,9 @@ import { EmbedDefinition } from '@tldraw/tlschema';
|
||||||
import { EMPTY_ARRAY } from '@tldraw/state';
|
import { EMPTY_ARRAY } from '@tldraw/state';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import { HistoryEntry } from '@tldraw/store';
|
import { HistoryEntry } from '@tldraw/store';
|
||||||
import { HTMLProps } from 'react';
|
|
||||||
import { IndexKey } from '@tldraw/utils';
|
import { IndexKey } from '@tldraw/utils';
|
||||||
import { JsonObject } from '@tldraw/utils';
|
import { JsonObject } from '@tldraw/utils';
|
||||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||||
import { MemoExoticComponent } from 'react';
|
|
||||||
import { Migrations } from '@tldraw/store';
|
import { Migrations } from '@tldraw/store';
|
||||||
import { NamedExoticComponent } from 'react';
|
import { NamedExoticComponent } from 'react';
|
||||||
import { PointerEventHandler } 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>;
|
export type DebugFlag<T> = DebugFlagDef<T> & Atom<T>;
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export const debugFlags: {
|
export const debugFlags: Record<string, DebugFlag<boolean>>;
|
||||||
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>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export const DEFAULT_ANIMATION_OPTIONS: {
|
export const DEFAULT_ANIMATION_OPTIONS: {
|
||||||
|
@ -1452,13 +1435,6 @@ export class Polyline2d extends Geometry2d {
|
||||||
_segments?: Edge2d[];
|
_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)
|
// @public (undocumented)
|
||||||
export function precise(A: VecLike): string;
|
export function precise(A: VecLike): string;
|
||||||
|
|
||||||
|
|
|
@ -27915,83 +27915,6 @@
|
||||||
},
|
},
|
||||||
"implementsTokenRanges": []
|
"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",
|
"kind": "Function",
|
||||||
"canonicalReference": "@tldraw/editor!precise:function(1)",
|
"canonicalReference": "@tldraw/editor!precise:function(1)",
|
||||||
|
|
|
@ -41,7 +41,6 @@ export {
|
||||||
type TLErrorBoundaryProps,
|
type TLErrorBoundaryProps,
|
||||||
} from './lib/components/ErrorBoundary'
|
} from './lib/components/ErrorBoundary'
|
||||||
export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
|
export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
|
||||||
export { PositionedOnCanvas } from './lib/components/PositionedOnCanvas'
|
|
||||||
export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
|
export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
|
||||||
export { ShapeIndicator, type TLShapeIndicatorComponent } from './lib/components/ShapeIndicator'
|
export { ShapeIndicator, type TLShapeIndicatorComponent } from './lib/components/ShapeIndicator'
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -99,7 +99,6 @@ export function Canvas({ className }: { className?: string }) {
|
||||||
>
|
>
|
||||||
{Background && <Background />}
|
{Background && <Background />}
|
||||||
<GridWrapper />
|
<GridWrapper />
|
||||||
<UiLogger />
|
|
||||||
<svg className="tl-svg-context">
|
<svg className="tl-svg-context">
|
||||||
<defs>
|
<defs>
|
||||||
{shapeSvgDefs}
|
{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() {
|
function SelectionForegroundWrapper() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const selectionRotation = useValue('selection rotation', () => editor.getSelectionRotation(), [
|
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 { TLShape, TLShapeId } from '@tldraw/tlschema'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEditor } from '../..'
|
|
||||||
import type { Editor } from '../editor/Editor'
|
import type { Editor } from '../editor/Editor'
|
||||||
import { ShapeUtil } from '../editor/shapes/ShapeUtil'
|
import { ShapeUtil } from '../editor/shapes/ShapeUtil'
|
||||||
|
import { useEditor } from '../hooks/useEditor'
|
||||||
import { useEditorComponents } from '../hooks/useEditorComponents'
|
import { useEditorComponents } from '../hooks/useEditorComponents'
|
||||||
import { OptionalErrorBoundary } from './ErrorBoundary'
|
import { OptionalErrorBoundary } from './ErrorBoundary'
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ import {
|
||||||
} from '../components/default-components/DefaultSnapIndictor'
|
} from '../components/default-components/DefaultSnapIndictor'
|
||||||
import { DefaultSpinner, TLSpinnerComponent } from '../components/default-components/DefaultSpinner'
|
import { DefaultSpinner, TLSpinnerComponent } from '../components/default-components/DefaultSpinner'
|
||||||
import { DefaultSvgDefs, TLSvgDefsComponent } from '../components/default-components/DefaultSvgDefs'
|
import { DefaultSvgDefs, TLSvgDefsComponent } from '../components/default-components/DefaultSvgDefs'
|
||||||
|
import { useShallowObjectIdentity } from './useIdentity'
|
||||||
|
|
||||||
export interface BaseEditorComponents {
|
export interface BaseEditorComponents {
|
||||||
Background: TLBackgroundComponent
|
Background: TLBackgroundComponent
|
||||||
|
@ -97,7 +98,11 @@ type ComponentsContextProviderProps = {
|
||||||
children: any
|
children: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditorComponentsProvider({ overrides, children }: ComponentsContextProviderProps) {
|
export function EditorComponentsProvider({
|
||||||
|
overrides = {},
|
||||||
|
children,
|
||||||
|
}: ComponentsContextProviderProps) {
|
||||||
|
const _overrides = useShallowObjectIdentity(overrides)
|
||||||
return (
|
return (
|
||||||
<EditorComponentsContext.Provider
|
<EditorComponentsContext.Provider
|
||||||
value={useMemo(
|
value={useMemo(
|
||||||
|
@ -127,9 +132,9 @@ export function EditorComponentsProvider({ overrides, children }: ComponentsCont
|
||||||
HoveredShapeIndicator: DefaultHoveredShapeIndicator,
|
HoveredShapeIndicator: DefaultHoveredShapeIndicator,
|
||||||
OnTheCanvas: null,
|
OnTheCanvas: null,
|
||||||
InFrontOfTheCanvas: null,
|
InFrontOfTheCanvas: null,
|
||||||
...overrides,
|
..._overrides,
|
||||||
}),
|
}),
|
||||||
[overrides]
|
[_overrides]
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -12,26 +12,25 @@ export const featureFlags: Record<string, DebugFlag<boolean>> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const debugFlags = {
|
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
|
||||||
|
{
|
||||||
|
defaults: { all: new Map<Element, number>() },
|
||||||
|
shouldStoreForSession: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export const debugFlags: Record<string, DebugFlag<boolean>> = {
|
||||||
// --- DEBUG VALUES ---
|
// --- DEBUG VALUES ---
|
||||||
preventDefaultLogging: createDebugValue('preventDefaultLogging', {
|
preventDefaultLogging: createDebugValue('preventDefaultLogging', {
|
||||||
defaults: { all: false },
|
defaults: { all: false },
|
||||||
}),
|
}),
|
||||||
pointerCaptureLogging: createDebugValue('pointerCaptureLogging', {
|
|
||||||
defaults: { all: false },
|
|
||||||
}),
|
|
||||||
pointerCaptureTracking: createDebugValue('pointerCaptureTracking', {
|
pointerCaptureTracking: createDebugValue('pointerCaptureTracking', {
|
||||||
defaults: { all: false },
|
defaults: { all: false },
|
||||||
}),
|
}),
|
||||||
pointerCaptureTrackingObject: createDebugValue(
|
|
||||||
'pointerCaptureTrackingObject',
|
|
||||||
// ideally we wouldn't store this mutable value in an atom but it's not
|
|
||||||
// a big deal for debug values
|
|
||||||
{
|
|
||||||
defaults: { all: new Map<Element, number>() },
|
|
||||||
shouldStoreForSession: false,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
elementRemovalLogging: createDebugValue('elementRemovalLogging', {
|
elementRemovalLogging: createDebugValue('elementRemovalLogging', {
|
||||||
defaults: { all: false },
|
defaults: { all: false },
|
||||||
}),
|
}),
|
||||||
|
@ -44,7 +43,6 @@ export const debugFlags = {
|
||||||
throwToBlob: createDebugValue('throwToBlob', {
|
throwToBlob: createDebugValue('throwToBlob', {
|
||||||
defaults: { all: false },
|
defaults: { all: false },
|
||||||
}),
|
}),
|
||||||
logMessages: createDebugValue('uiLog', { defaults: { all: [] as any[] } }),
|
|
||||||
resetConnectionEveryPing: createDebugValue('resetConnectionEveryPing', {
|
resetConnectionEveryPing: createDebugValue('resetConnectionEveryPing', {
|
||||||
defaults: { all: false },
|
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 ---
|
// --- 2. USE ---
|
||||||
// In normal code, read from debug flags directly by calling .value on them:
|
// In normal code, read from debug flags directly by calling .value on them:
|
||||||
// if (debugFlags.preventDefaultLogging.value) { ... }
|
// if (debugFlags.preventDefaultLogging.value) { ... }
|
||||||
|
|
|
@ -14,7 +14,7 @@ whatever reason.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { debugFlags } from './debug-flags'
|
import { debugFlags, pointerCaptureTrackingObject } from './debug-flags'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function loopToHtmlElement(elm: Element): HTMLElement {
|
export function loopToHtmlElement(elm: Element): HTMLElement {
|
||||||
|
@ -49,10 +49,8 @@ export function setPointerCapture(
|
||||||
) {
|
) {
|
||||||
element.setPointerCapture(event.pointerId)
|
element.setPointerCapture(event.pointerId)
|
||||||
if (debugFlags.pointerCaptureTracking.get()) {
|
if (debugFlags.pointerCaptureTracking.get()) {
|
||||||
const trackingObj = debugFlags.pointerCaptureTrackingObject.get()
|
const trackingObj = pointerCaptureTrackingObject.get()
|
||||||
trackingObj.set(element, (trackingObj.get(element) ?? 0) + 1)
|
trackingObj.set(element, (trackingObj.get(element) ?? 0) + 1)
|
||||||
}
|
|
||||||
if (debugFlags.pointerCaptureLogging.get()) {
|
|
||||||
console.warn('setPointerCapture called on element:', element, event)
|
console.warn('setPointerCapture called on element:', element, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +66,7 @@ export function releasePointerCapture(
|
||||||
|
|
||||||
element.releasePointerCapture(event.pointerId)
|
element.releasePointerCapture(event.pointerId)
|
||||||
if (debugFlags.pointerCaptureTracking.get()) {
|
if (debugFlags.pointerCaptureTracking.get()) {
|
||||||
const trackingObj = debugFlags.pointerCaptureTrackingObject.get()
|
const trackingObj = pointerCaptureTrackingObject.get()
|
||||||
if (trackingObj.get(element) === 1) {
|
if (trackingObj.get(element) === 1) {
|
||||||
trackingObj.delete(element)
|
trackingObj.delete(element)
|
||||||
} else if (trackingObj.has(element)) {
|
} else if (trackingObj.has(element)) {
|
||||||
|
@ -77,9 +75,7 @@ export function releasePointerCapture(
|
||||||
console.warn('Release without capture')
|
console.warn('Release without capture')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (debugFlags.pointerCaptureLogging.get()) {
|
console.warn('releasePointerCapture called on element:', element, event)
|
||||||
console.warn('releasePointerCapture called on element:', element, event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|
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'
|
export { ZoomTool } from './lib/tools/ZoomTool/ZoomTool'
|
||||||
// UI
|
// UI
|
||||||
export { TldrawUi, type TldrawUiBaseProps, type TldrawUiProps } from './lib/ui/TldrawUi'
|
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 { 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 { OfflineIndicator } from './lib/ui/components/OfflineIndicator/OfflineIndicator'
|
||||||
export { Spinner } from './lib/ui/components/Spinner'
|
export { Spinner } from './lib/ui/components/Spinner'
|
||||||
export { Button, type TLUiButtonProps } from './lib/ui/components/primitives/Button'
|
export { Button, type TLUiButtonProps } from './lib/ui/components/primitives/Button'
|
||||||
export { Icon, type TLUiIconProps } from './lib/ui/components/primitives/Icon'
|
export { Icon, type TLUiIconProps } from './lib/ui/components/primitives/Icon'
|
||||||
export { Input, type TLUiInputProps } from './lib/ui/components/primitives/Input'
|
export { Input, type TLUiInputProps } from './lib/ui/components/primitives/Input'
|
||||||
export {
|
export {
|
||||||
compactMenuItems,
|
TldrawUiContextProvider,
|
||||||
findMenuItem,
|
type TldrawUiContextProviderProps,
|
||||||
menuCustom,
|
} from './lib/ui/context/TldrawUiContextProvider'
|
||||||
menuGroup,
|
|
||||||
menuItem,
|
|
||||||
menuSubmenu,
|
|
||||||
type TLUiCustomMenuItem,
|
|
||||||
type TLUiMenuChild,
|
|
||||||
type TLUiMenuGroup,
|
|
||||||
type TLUiMenuItem,
|
|
||||||
type TLUiMenuSchema,
|
|
||||||
type TLUiSubMenu,
|
|
||||||
} from './lib/ui/hooks/menuHelpers'
|
|
||||||
export {
|
export {
|
||||||
useActions,
|
useActions,
|
||||||
type TLUiActionItem,
|
type TLUiActionItem,
|
||||||
type TLUiActionsContextType,
|
type TLUiActionsContextType,
|
||||||
} from './lib/ui/hooks/useActions'
|
} from './lib/ui/context/actions'
|
||||||
export {
|
export { AssetUrlsProvider, useAssetUrls } from './lib/ui/context/asset-urls'
|
||||||
useActionsMenuSchema,
|
export { BreakPointProvider, useBreakpoint } from './lib/ui/context/breakpoints'
|
||||||
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'
|
|
||||||
export {
|
export {
|
||||||
useDialogs,
|
useDialogs,
|
||||||
type TLUiDialog,
|
type TLUiDialog,
|
||||||
type TLUiDialogProps,
|
type TLUiDialogProps,
|
||||||
type TLUiDialogsContextType,
|
type TLUiDialogsContextType,
|
||||||
} from './lib/ui/hooks/useDialogsProvider'
|
} from './lib/ui/context/dialogs'
|
||||||
export {
|
export {
|
||||||
UiEventsProvider,
|
UiEventsProvider,
|
||||||
useUiEvents,
|
useUiEvents,
|
||||||
|
@ -97,32 +70,20 @@ export {
|
||||||
type TLUiEventHandler,
|
type TLUiEventHandler,
|
||||||
type TLUiEventMap,
|
type TLUiEventMap,
|
||||||
type TLUiEventSource,
|
type TLUiEventSource,
|
||||||
} from './lib/ui/hooks/useEventsProvider'
|
} from './lib/ui/context/events'
|
||||||
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'
|
|
||||||
export {
|
export {
|
||||||
useToasts,
|
useToasts,
|
||||||
type TLUiToast,
|
type TLUiToast,
|
||||||
type TLUiToastAction,
|
type TLUiToastAction,
|
||||||
type TLUiToastsContextType,
|
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 {
|
export {
|
||||||
toolbarItem,
|
toolbarItem,
|
||||||
useToolbarSchema,
|
useToolbarSchema,
|
||||||
|
@ -170,9 +131,150 @@ export {
|
||||||
type TldrawFile,
|
type TldrawFile,
|
||||||
} from './lib/utils/tldr/file'
|
} from './lib/utils/tldr/file'
|
||||||
|
|
||||||
import * as Dialog from './lib/ui/components/primitives/Dialog'
|
// Minimap default component
|
||||||
import * as DropdownMenu from './lib/ui/components/primitives/DropdownMenu'
|
export { DefaultMinimap } from './lib/ui/components/Minimap/DefaultMinimap'
|
||||||
|
|
||||||
// N.B. Preserve order of import / export here with this comment.
|
// Helper to unwrap label from action items
|
||||||
// Sometimes this can cause an import problem depending on build setup downstream.
|
export { unwrapLabel } from './lib/ui/context/actions'
|
||||||
export { Dialog, DropdownMenu }
|
|
||||||
|
// 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,
|
ErrorScreen,
|
||||||
LoadingScreen,
|
LoadingScreen,
|
||||||
StoreSnapshot,
|
StoreSnapshot,
|
||||||
|
TLEditorComponents,
|
||||||
TLOnMountHandler,
|
TLOnMountHandler,
|
||||||
TLRecord,
|
TLRecord,
|
||||||
TLStore,
|
TLStore,
|
||||||
TLStoreWithStatus,
|
TLStoreWithStatus,
|
||||||
TldrawEditor,
|
TldrawEditor,
|
||||||
TldrawEditorBaseProps,
|
TldrawEditorBaseProps,
|
||||||
TldrawEditorProps,
|
|
||||||
assert,
|
assert,
|
||||||
useEditor,
|
useEditor,
|
||||||
useShallowArrayIdentity,
|
useShallowArrayIdentity,
|
||||||
|
@ -31,29 +31,37 @@ import { defaultShapeUtils } from './defaultShapeUtils'
|
||||||
import { registerDefaultSideEffects } from './defaultSideEffects'
|
import { registerDefaultSideEffects } from './defaultSideEffects'
|
||||||
import { defaultTools } from './defaultTools'
|
import { defaultTools } from './defaultTools'
|
||||||
import { TldrawUi, TldrawUiProps } from './ui/TldrawUi'
|
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 { usePreloadAssets } from './ui/hooks/usePreloadAssets'
|
||||||
import { useDefaultEditorAssetsWithOverrides } from './utils/static-assets/assetUrls'
|
import { useDefaultEditorAssetsWithOverrides } from './utils/static-assets/assetUrls'
|
||||||
|
|
||||||
|
/**@public */
|
||||||
|
export type TLComponents = TLEditorComponents & TLUiComponents
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TldrawProps = TldrawEditorBaseProps &
|
export type TldrawProps =
|
||||||
(
|
// combine components from base editor and ui
|
||||||
| {
|
(Omit<TldrawUiProps, 'components'> &
|
||||||
store: TLStore | TLStoreWithStatus
|
Omit<TldrawEditorBaseProps, 'components'> & {
|
||||||
}
|
components?: TLComponents
|
||||||
| {
|
}) &
|
||||||
store?: undefined
|
// external content
|
||||||
persistenceKey?: string
|
Partial<TLExternalContentProps> &
|
||||||
sessionId?: string
|
// store stuff
|
||||||
defaultName?: string
|
(| {
|
||||||
/**
|
store: TLStore | TLStoreWithStatus
|
||||||
* A snapshot to load for the store's initial data / schema.
|
}
|
||||||
*/
|
| {
|
||||||
snapshot?: StoreSnapshot<TLRecord>
|
store?: undefined
|
||||||
}
|
persistenceKey?: string
|
||||||
) &
|
sessionId?: string
|
||||||
TldrawUiProps &
|
defaultName?: string
|
||||||
Partial<TLExternalContentProps>
|
/**
|
||||||
|
* A snapshot to load for the store's initial data / schema.
|
||||||
|
*/
|
||||||
|
snapshot?: StoreSnapshot<TLRecord>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function Tldraw(props: TldrawProps) {
|
export function Tldraw(props: TldrawProps) {
|
||||||
|
@ -64,31 +72,37 @@ export function Tldraw(props: TldrawProps) {
|
||||||
acceptedImageMimeTypes,
|
acceptedImageMimeTypes,
|
||||||
acceptedVideoMimeTypes,
|
acceptedVideoMimeTypes,
|
||||||
onMount,
|
onMount,
|
||||||
|
components = {},
|
||||||
|
shapeUtils = [],
|
||||||
|
tools = [],
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const components = useShallowObjectIdentity(rest.components ?? {})
|
const _components = useShallowObjectIdentity(components)
|
||||||
const shapeUtils = useShallowArrayIdentity(rest.shapeUtils ?? [])
|
const componentsWithDefault = useMemo(
|
||||||
const tools = useShallowArrayIdentity(rest.tools ?? [])
|
() => ({
|
||||||
|
Scribble: TldrawScribble,
|
||||||
|
CollaboratorScribble: TldrawScribble,
|
||||||
|
SelectionForeground: TldrawSelectionForeground,
|
||||||
|
SelectionBackground: TldrawSelectionBackground,
|
||||||
|
Handles: TldrawHandles,
|
||||||
|
HoveredShapeIndicator: TldrawHoveredShapeIndicator,
|
||||||
|
..._components,
|
||||||
|
}),
|
||||||
|
[_components]
|
||||||
|
)
|
||||||
|
|
||||||
const withDefaults: TldrawEditorProps = {
|
const _shapeUtils = useShallowArrayIdentity(shapeUtils)
|
||||||
initialState: 'select',
|
const shapeUtilsWithDefaults = useMemo(
|
||||||
...rest,
|
() => [...defaultShapeUtils, ..._shapeUtils],
|
||||||
components: useMemo(
|
[_shapeUtils]
|
||||||
() => ({
|
)
|
||||||
Scribble: TldrawScribble,
|
|
||||||
CollaboratorScribble: TldrawScribble,
|
const _tools = useShallowArrayIdentity(tools)
|
||||||
SelectionForeground: TldrawSelectionForeground,
|
const toolsWithDefaults = useMemo(
|
||||||
SelectionBackground: TldrawSelectionBackground,
|
() => [...defaultTools, ...defaultShapeTools, ..._tools],
|
||||||
Handles: TldrawHandles,
|
[_tools]
|
||||||
HoveredShapeIndicator: TldrawHoveredShapeIndicator,
|
)
|
||||||
...components,
|
|
||||||
}),
|
|
||||||
[components]
|
|
||||||
),
|
|
||||||
shapeUtils: useMemo(() => [...defaultShapeUtils, ...shapeUtils], [shapeUtils]),
|
|
||||||
tools: useMemo(() => [...defaultTools, ...defaultShapeTools, ...tools], [tools]),
|
|
||||||
}
|
|
||||||
|
|
||||||
const assets = useDefaultEditorAssetsWithOverrides(rest.assetUrls)
|
const assets = useDefaultEditorAssetsWithOverrides(rest.assetUrls)
|
||||||
|
|
||||||
|
@ -103,11 +117,14 @@ export function Tldraw(props: TldrawProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TldrawEditor {...withDefaults}>
|
<TldrawEditor
|
||||||
<TldrawUi {...withDefaults}>
|
initialState="select"
|
||||||
<ContextMenu>
|
{...rest}
|
||||||
<Canvas />
|
components={componentsWithDefault}
|
||||||
</ContextMenu>
|
shapeUtils={shapeUtilsWithDefaults}
|
||||||
|
tools={toolsWithDefaults}
|
||||||
|
>
|
||||||
|
<TldrawUi {...rest} components={componentsWithDefault}>
|
||||||
<InsideOfEditorContext
|
<InsideOfEditorContext
|
||||||
maxImageDimension={maxImageDimension}
|
maxImageDimension={maxImageDimension}
|
||||||
maxAssetSize={maxAssetSize}
|
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.
|
// We put these hooks into a component here so that they can run inside of the context provided by TldrawEditor.
|
||||||
function InsideOfEditorContext({
|
function InsideOfEditorContext({
|
||||||
maxImageDimension = 1000,
|
maxImageDimension = 1000,
|
||||||
maxAssetSize = 10 * 1024 * 1024, // 10mb
|
maxAssetSize = 10 * 1024 * 1024, // 10mb
|
||||||
acceptedImageMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'],
|
acceptedImageMimeTypes = defaultAcceptedImageMimeTypes,
|
||||||
acceptedVideoMimeTypes = ['video/mp4', 'video/quicktime'],
|
acceptedVideoMimeTypes = defaultAcceptedVideoMimeTypes,
|
||||||
onMount,
|
onMount,
|
||||||
}: Partial<TLExternalContentProps & { onMount: TLOnMountHandler }>) {
|
}: Partial<TLExternalContentProps & { onMount: TLOnMountHandler }>) {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
@ -156,7 +182,10 @@ function InsideOfEditorContext({
|
||||||
if (editor) return onMountEvent?.(editor)
|
if (editor) return onMountEvent?.(editor)
|
||||||
}, [editor, onMountEvent])
|
}, [editor, onMountEvent])
|
||||||
|
|
||||||
return null
|
const { ContextMenu } = useTldrawUiComponents()
|
||||||
|
if (!ContextMenu) return <Canvas />
|
||||||
|
|
||||||
|
return <ContextMenu canvas={<Canvas />} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// duped from tldraw editor
|
// 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).
|
// The maximum size (in bytes) of an asset. Assets larger than this will be rejected. Defaults to 10mb (10 * 1024 * 1024).
|
||||||
maxAssetSize: number
|
maxAssetSize: number
|
||||||
// The mime types of images that are allowed to be handled. Defaults to ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'].
|
// 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'].
|
// 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(
|
export function registerDefaultExternalContentHandlers(
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
.tlui-button:disabled {
|
.tlui-button:disabled {
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tlui-button:disabled .tlui-kbd {
|
.tlui-button:disabled .tlui-kbd {
|
||||||
|
@ -310,6 +311,9 @@
|
||||||
.tlui-buttons__horizontal > *:nth-last-child(1) {
|
.tlui-buttons__horizontal > *:nth-last-child(1) {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
.tlui-buttons__horizontal > *:only-child {
|
||||||
|
width: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Button Grid */
|
/* Button Grid */
|
||||||
|
|
||||||
|
@ -504,7 +508,6 @@
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-template-columns: auto;
|
grid-template-columns: auto;
|
||||||
grid-auto-columns: minmax(1em, auto);
|
grid-auto-columns: minmax(1em, auto);
|
||||||
gap: 1px;
|
|
||||||
align-self: bottom;
|
align-self: bottom;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
margin-left: var(--space-4);
|
margin-left: var(--space-4);
|
||||||
|
@ -860,6 +863,10 @@
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tlui-toolbar__extras:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tlui-toolbar__extras__controls {
|
.tlui-toolbar__extras__controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -928,6 +935,10 @@
|
||||||
|
|
||||||
/* ---------------------- Menu ---------------------- */
|
/* ---------------------- Menu ---------------------- */
|
||||||
|
|
||||||
|
.tlui-menu:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tlui-menu {
|
.tlui-menu {
|
||||||
z-index: var(--layer-menus);
|
z-index: var(--layer-menus);
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
@ -954,24 +965,15 @@
|
||||||
stroke-width: 1px;
|
stroke-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tlui-menu__group[data-size='large'] {
|
.tlui-menu__group:empty {
|
||||||
min-width: initial;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tlui-menu__group[data-size='medium'] {
|
.tlui-menu__group {
|
||||||
min-width: 144px;
|
border-bottom: 1px solid var(--color-divider);
|
||||||
}
|
}
|
||||||
|
.tlui-menu__group:nth-last-of-type(1) {
|
||||||
.tlui-menu__group[data-size='small'] {
|
border-bottom: none;
|
||||||
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__submenu__trigger[data-state='open']:not(:hover)::after {
|
.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%);
|
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 ------------------ */
|
/* ------------------ Actions Menu ------------------ */
|
||||||
|
|
||||||
.tlui-actions-menu {
|
.tlui-actions-menu {
|
||||||
|
@ -1105,7 +1128,7 @@
|
||||||
|
|
||||||
/* ------------------- Navigation ------------------- */
|
/* ------------------- Navigation ------------------- */
|
||||||
|
|
||||||
.tlui-navigation-zone {
|
.tlui-navigation-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: min-content;
|
width: min-content;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -1116,7 +1139,7 @@
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tlui-navigation-zone::before {
|
.tlui-navigation-panel::before {
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -1129,16 +1152,16 @@
|
||||||
background-color: var(--color-low);
|
background-color: var(--color-low);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tlui-navigation-zone__toggle .tlui-icon {
|
.tlui-navigation-panel__toggle .tlui-icon {
|
||||||
opacity: 0.24;
|
opacity: 0.24;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tlui-navigation-zone__toggle:active .tlui-icon {
|
.tlui-navigation-panel__toggle:active .tlui-icon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.tlui-navigation-zone__toggle:hover .tlui-icon {
|
.tlui-navigation-panel__toggle:hover .tlui-icon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,26 +2,24 @@ import { ToastProvider } from '@radix-ui/react-toast'
|
||||||
import { useEditor, useValue } from '@tldraw/editor'
|
import { useEditor, useValue } from '@tldraw/editor'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import React, { ReactNode } from 'react'
|
import React, { ReactNode } from 'react'
|
||||||
import { TldrawUiContextProvider, TldrawUiContextProviderProps } from './TldrawUiContextProvider'
|
|
||||||
import { TLUiAssetUrlOverrides } from './assetUrls'
|
import { TLUiAssetUrlOverrides } from './assetUrls'
|
||||||
import { BackToContent } from './components/BackToContent'
|
|
||||||
import { DebugPanel } from './components/DebugPanel'
|
import { DebugPanel } from './components/DebugPanel'
|
||||||
import { Dialogs } from './components/Dialogs'
|
import { Dialogs } from './components/Dialogs'
|
||||||
import { FollowingIndicator } from './components/FollowingIndicator'
|
import { FollowingIndicator } from './components/FollowingIndicator'
|
||||||
import { HelpMenu } from './components/HelpMenu'
|
|
||||||
import { MenuZone } from './components/MenuZone'
|
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 { ToastViewport, Toasts } from './components/Toasts'
|
||||||
import { Toolbar } from './components/Toolbar/Toolbar'
|
|
||||||
import { Button } from './components/primitives/Button'
|
import { Button } from './components/primitives/Button'
|
||||||
import { useActions } from './hooks/useActions'
|
import {
|
||||||
import { useBreakpoint } from './hooks/useBreakpoint'
|
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 { useNativeClipboardEvents } from './hooks/useClipboardEvents'
|
||||||
import { useEditorEvents } from './hooks/useEditorEvents'
|
import { useEditorEvents } from './hooks/useEditorEvents'
|
||||||
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
|
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
|
||||||
|
import { useRelevantStyles } from './hooks/useRevelantStyles'
|
||||||
import { useTranslation } from './hooks/useTranslation/useTranslation'
|
import { useTranslation } from './hooks/useTranslation/useTranslation'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,6 +45,11 @@ export interface TldrawUiBaseProps {
|
||||||
*/
|
*/
|
||||||
hideUi?: boolean
|
hideUi?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides for the UI components.
|
||||||
|
*/
|
||||||
|
components?: TLUiComponents
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component to use for the share zone (will be deprecated)
|
* A component to use for the share zone (will be deprecated)
|
||||||
*/
|
*/
|
||||||
|
@ -76,10 +79,11 @@ export const TldrawUi = React.memo(function TldrawUi({
|
||||||
renderDebugMenuItems,
|
renderDebugMenuItems,
|
||||||
children,
|
children,
|
||||||
hideUi,
|
hideUi,
|
||||||
|
components,
|
||||||
...rest
|
...rest
|
||||||
}: TldrawUiProps) {
|
}: TldrawUiProps) {
|
||||||
return (
|
return (
|
||||||
<TldrawUiContextProvider {...rest}>
|
<TldrawUiContextProvider {...rest} components={components}>
|
||||||
<TldrawUiInner
|
<TldrawUiInner
|
||||||
hideUi={hideUi}
|
hideUi={hideUi}
|
||||||
shareZone={shareZone}
|
shareZone={shareZone}
|
||||||
|
@ -116,11 +120,7 @@ const TldrawUiInner = React.memo(function TldrawUiInner({
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const TldrawUiContent = React.memo(function TldrawUI({
|
const TldrawUiContent = React.memo(function TldrawUI({ shareZone, topZone }: TldrawUiContentProps) {
|
||||||
shareZone,
|
|
||||||
topZone,
|
|
||||||
renderDebugMenuItems,
|
|
||||||
}: TldrawUiContentProps) {
|
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const msg = useTranslation()
|
const msg = useTranslation()
|
||||||
const breakpoint = useBreakpoint()
|
const breakpoint = useBreakpoint()
|
||||||
|
@ -130,6 +130,8 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
||||||
const isFocusMode = useValue('focus', () => editor.getInstanceState().isFocusMode, [editor])
|
const isFocusMode = useValue('focus', () => editor.getInstanceState().isFocusMode, [editor])
|
||||||
const isDebugMode = useValue('debug', () => editor.getInstanceState().isDebugMode, [editor])
|
const isDebugMode = useValue('debug', () => editor.getInstanceState().isDebugMode, [editor])
|
||||||
|
|
||||||
|
const { StylePanel, Toolbar, HelpMenu, NavigationPanel, HelperButtons } = useTldrawUiComponents()
|
||||||
|
|
||||||
useKeyboardShortcuts()
|
useKeyboardShortcuts()
|
||||||
useNativeClipboardEvents()
|
useNativeClipboardEvents()
|
||||||
useEditorEvents()
|
useEditorEvents()
|
||||||
|
@ -159,29 +161,21 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
||||||
<div className="tlui-layout__top">
|
<div className="tlui-layout__top">
|
||||||
<div className="tlui-layout__top__left">
|
<div className="tlui-layout__top__left">
|
||||||
<MenuZone />
|
<MenuZone />
|
||||||
<div className="tlui-helper-buttons">
|
{HelperButtons && <HelperButtons />}
|
||||||
<ExitPenMode />
|
|
||||||
<BackToContent />
|
|
||||||
<StopFollowing />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="tlui-layout__top__center">{topZone}</div>
|
<div className="tlui-layout__top__center">{topZone}</div>
|
||||||
<div className="tlui-layout__top__right">
|
<div className="tlui-layout__top__right">
|
||||||
{shareZone}
|
{shareZone}
|
||||||
{breakpoint >= 5 && !isReadonlyMode && (
|
{StylePanel && breakpoint >= 5 && !isReadonlyMode && <_StylePanel />}
|
||||||
<div className="tlui-style-panel__wrapper">
|
|
||||||
<StylePanel />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="tlui-layout__bottom">
|
<div className="tlui-layout__bottom">
|
||||||
<div className="tlui-layout__bottom__main">
|
<div className="tlui-layout__bottom__main">
|
||||||
<NavigationZone />
|
{NavigationPanel && <NavigationPanel />}
|
||||||
<Toolbar />
|
{Toolbar && <Toolbar />}
|
||||||
{breakpoint >= 4 && <HelpMenu />}
|
{HelpMenu && <HelpMenu />}
|
||||||
</div>
|
</div>
|
||||||
{isDebugMode && <DebugPanel renderDebugMenuItems={renderDebugMenuItems ?? null} />}
|
{isDebugMode && <DebugPanel />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -193,3 +187,11 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
||||||
</ToastProvider>
|
</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 {
|
import { debugFlags, track, useEditor, useValue, Vec } from '@tldraw/editor'
|
||||||
createShapeId,
|
import { memo, useEffect, useRef, useState } from 'react'
|
||||||
DebugFlag,
|
import { useTldrawUiComponents } from '../context/components'
|
||||||
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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const DebugPanel = React.memo(function DebugPanel({
|
export const DebugPanel = memo(function DebugPanel() {
|
||||||
renderDebugMenuItems,
|
const { DebugMenu } = useTldrawUiComponents()
|
||||||
}: {
|
|
||||||
renderDebugMenuItems: (() => React.ReactNode) | null
|
|
||||||
}) {
|
|
||||||
const msg = useTranslation()
|
|
||||||
const showFps = useValue('show_fps', () => debugFlags.showFps.get(), [debugFlags])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tlui-debug-panel">
|
<div className="tlui-debug-panel">
|
||||||
<CurrentState />
|
<CurrentState />
|
||||||
{showFps && <FPS />}
|
<FPS />
|
||||||
<ShapeCount />
|
{DebugMenu && <DebugMenu />}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function useTick(isEnabled = true) {
|
function useTick(isEnabled = true) {
|
||||||
const [_, setTick] = React.useState(0)
|
const [_, setTick] = useState(0)
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
const update = () => setTick((tick) => tick + 1)
|
const update = () => setTick((tick) => tick + 1)
|
||||||
editor.on('tick', update)
|
editor.on('tick', update)
|
||||||
|
@ -107,9 +55,13 @@ const CurrentState = track(function CurrentState() {
|
||||||
})
|
})
|
||||||
|
|
||||||
function FPS() {
|
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
|
const TICK_LENGTH = 250
|
||||||
let maxKnownFps = 0
|
let maxKnownFps = 0
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
@ -168,294 +120,9 @@ function FPS() {
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [])
|
}, [showFps])
|
||||||
|
|
||||||
|
if (!showFps) return null
|
||||||
|
|
||||||
return <div ref={fpsRef} />
|
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 * as _Dialog from '@radix-ui/react-dialog'
|
||||||
import { useContainer } from '@tldraw/editor'
|
import { useContainer } from '@tldraw/editor'
|
||||||
import React, { useCallback } from 'react'
|
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 Dialog = ({ id, component: ModalContent, onClose }: TLUiDialog) => {
|
||||||
const { removeDialog } = useDialogs()
|
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 { T, TLBaseShape, track, useEditor } from '@tldraw/editor'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { TLUiDialogProps } from '../hooks/useDialogsProvider'
|
import { TLUiDialogProps } from '../context/dialogs'
|
||||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||||
import { Button } from './primitives/Button'
|
import { Button } from './primitives/Button'
|
||||||
import * as Dialog from './primitives/Dialog'
|
import { DialogBody, DialogCloseButton, DialogFooter, DialogHeader } from './primitives/Dialog'
|
||||||
import { Input } from './primitives/Input'
|
import { Input } from './primitives/Input'
|
||||||
|
|
||||||
// A url can either be invalid, or valid with a protocol, or valid without a protocol.
|
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog.Header>
|
<DialogHeader>
|
||||||
<Dialog.Title>{msg('edit-link-dialog.title')}</Dialog.Title>
|
<DialogTitle>{msg('edit-link-title')}</DialogTitle>
|
||||||
<Dialog.CloseButton />
|
<DialogCloseButton />
|
||||||
</Dialog.Header>
|
</DialogHeader>
|
||||||
<Dialog.Body>
|
<DialogBody>
|
||||||
<div className="tlui-edit-link-dialog">
|
<div className="tlui-edit-link-dialog">
|
||||||
<Input
|
<Input
|
||||||
ref={rInput}
|
ref={rInput}
|
||||||
className="tlui-edit-link-dialog__input"
|
className="tlui-edit-link-dialog__input"
|
||||||
label="edit-link-dialog.url"
|
label="edit-link-url"
|
||||||
autofocus
|
autofocus
|
||||||
value={urlInputState.actual}
|
value={urlInputState.actual}
|
||||||
onValueChange={handleChange}
|
onValueChange={handleChange}
|
||||||
onComplete={handleComplete}
|
onComplete={handleComplete}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>{urlInputState.valid ? msg('edit-link-detail') : msg('edit-link-invalid-url')}</div>
|
||||||
{urlInputState.valid
|
|
||||||
? msg('edit-link-dialog.detail')
|
|
||||||
: msg('edit-link-dialog.invalid-url')}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Body>
|
</DialogBody>
|
||||||
<Dialog.Footer className="tlui-dialog__footer__actions">
|
<DialogFooter className="tlui-dialog__footer__actions">
|
||||||
<Button type="normal" onClick={handleCancel} onTouchEnd={handleCancel}>
|
<Button type="normal" onClick={handleCancel} onTouchEnd={handleCancel}>
|
||||||
{msg('edit-link-dialog.cancel')}
|
{msg('edit-link-cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
{isRemoving ? (
|
{isRemoving ? (
|
||||||
<Button type={'danger'} onTouchEnd={handleClear} onClick={handleClear}>
|
<Button type={'danger'} onTouchEnd={handleClear} onClick={handleClear}>
|
||||||
{msg('edit-link-dialog.clear')}
|
{msg('edit-link-clear')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
@ -171,10 +168,10 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
|
||||||
onTouchEnd={handleComplete}
|
onTouchEnd={handleComplete}
|
||||||
onClick={handleComplete}
|
onClick={handleComplete}
|
||||||
>
|
>
|
||||||
{msg('edit-link-dialog.save')}
|
{msg('edit-link-save')}
|
||||||
</Button>
|
</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 { EMBED_DEFINITIONS, EmbedDefinition, track, useEditor } from '@tldraw/editor'
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import { TLEmbedResult, getEmbedInfo } from '../../utils/embeds/embeds'
|
import { TLEmbedResult, getEmbedInfo } from '../../utils/embeds/embeds'
|
||||||
import { useAssetUrls } from '../hooks/useAssetUrls'
|
import { useAssetUrls } from '../context/asset-urls'
|
||||||
import { TLUiDialogProps } from '../hooks/useDialogsProvider'
|
import { TLUiDialogProps } from '../context/dialogs'
|
||||||
import { untranslated, useTranslation } from '../hooks/useTranslation/useTranslation'
|
import { untranslated, useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||||
import { Button } from './primitives/Button'
|
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 { Icon } from './primitives/Icon'
|
||||||
import { Input } from './primitives/Input'
|
import { Input } from './primitives/Input'
|
||||||
|
|
||||||
|
@ -29,20 +30,20 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog.Header>
|
<DialogHeader>
|
||||||
<Dialog.Title>
|
<DialogTitle>
|
||||||
{embedDefinition
|
{embedDefinition
|
||||||
? `${msg('embed-dialog.title')} — ${embedDefinition.title}`
|
? `${msg('embed-title')} — ${embedDefinition.title}`
|
||||||
: msg('embed-dialog.title')}
|
: msg('embed-title')}
|
||||||
</Dialog.Title>
|
</DialogTitle>
|
||||||
<Dialog.CloseButton />
|
<DialogCloseButton />
|
||||||
</Dialog.Header>
|
</DialogHeader>
|
||||||
{embedDefinition ? (
|
{embedDefinition ? (
|
||||||
<>
|
<>
|
||||||
<Dialog.Body className="tlui-embed-dialog__enter">
|
<DialogBody className="tlui-embed-dialog__enter">
|
||||||
<Input
|
<Input
|
||||||
className="tlui-embed-dialog__input"
|
className="tlui-embed-dialog__input"
|
||||||
label="embed-dialog.url"
|
label="embed-url"
|
||||||
placeholder="http://example.com"
|
placeholder="http://example.com"
|
||||||
autofocus
|
autofocus
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
|
@ -67,7 +68,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
||||||
/>
|
/>
|
||||||
{url === '' ? (
|
{url === '' ? (
|
||||||
<div className="tlui-embed-dialog__instruction">
|
<div className="tlui-embed-dialog__instruction">
|
||||||
<span>{msg('embed-dialog.instruction')}</span>{' '}
|
<span>{msg('embed-instruction')}</span>{' '}
|
||||||
{embedDefinition.instructionLink && (
|
{embedDefinition.instructionLink && (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -82,11 +83,11 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="tlui-embed-dialog__warning">
|
<div className="tlui-embed-dialog__warning">
|
||||||
{showError ? msg('embed-dialog.invalid-url') : '\xa0'}
|
{showError ? msg('embed-invalid-url') : '\xa0'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Dialog.Body>
|
</DialogBody>
|
||||||
<Dialog.Footer className="tlui-dialog__footer__actions">
|
<DialogFooter className="tlui-dialog__footer__actions">
|
||||||
<Button
|
<Button
|
||||||
type="normal"
|
type="normal"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -94,14 +95,14 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
||||||
setEmbedInfoForUrl(null)
|
setEmbedInfoForUrl(null)
|
||||||
setUrl('')
|
setUrl('')
|
||||||
}}
|
}}
|
||||||
label="embed-dialog.back"
|
label="embed-back"
|
||||||
/>
|
/>
|
||||||
<div className="tlui-embed__spacer" />
|
<div className="tlui-embed__spacer" />
|
||||||
<Button type="normal" label="embed-dialog.cancel" onClick={onClose} />
|
<Button type="normal" label="embed-cancel" onClick={onClose} />
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
disabled={!embedInfoForUrl}
|
disabled={!embedInfoForUrl}
|
||||||
label="embed-dialog.create"
|
label="embed-create"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!embedInfoForUrl) return
|
if (!embedInfoForUrl) return
|
||||||
|
|
||||||
|
@ -115,11 +116,11 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
||||||
onClose()
|
onClose()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Dialog.Footer>
|
</DialogFooter>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Dialog.Body className="tlui-embed-dialog__list">
|
<DialogBody className="tlui-embed-dialog__list">
|
||||||
{EMBED_DEFINITIONS.map((def) => {
|
{EMBED_DEFINITIONS.map((def) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -135,7 +136,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
||||||
</Button>
|
</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 { useEditor } from '@tldraw/editor'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useActions } from '../hooks/useActions'
|
import { useActions } from '../../context/actions'
|
||||||
import { Button } from './primitives/Button'
|
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem'
|
||||||
|
|
||||||
export function BackToContent() {
|
export function BackToContent() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
const actions = useActions()
|
const actions = useActions()
|
||||||
const action = actions['back-to-content']
|
|
||||||
|
|
||||||
const [showBackToContent, setShowBackToContent] = useState(false)
|
const [showBackToContent, setShowBackToContent] = useState(false)
|
||||||
|
|
||||||
|
@ -42,12 +41,10 @@ export function BackToContent() {
|
||||||
if (!showBackToContent) return null
|
if (!showBackToContent) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<TldrawUiMenuItem
|
||||||
iconLeft={action.icon}
|
{...actions['back-to-content']}
|
||||||
label={action.label}
|
onSelect={() => {
|
||||||
type="low"
|
actions['back-to-content'].onSelect('helper-buttons')
|
||||||
onClick={() => {
|
|
||||||
action.onSelect('helper-buttons')
|
|
||||||
setShowBackToContent(false)
|
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