menu: rework File menu / ensure Export menu is present (#2783)

<img width="428" alt="Screenshot 2024-02-16 at 16 46 28"
src="https://github.com/tldraw/tldraw/assets/469604/334cd0db-d9d5-4993-8012-c6985173edfb">


- re-orders to be the normative New / Open / Save order — we shouldn't
be messing with this conventional ordering
- removes the "Don't ask again" from New/Open dialogs because they're
non-undoable and not what _anybody_ should ever select. we shouldn't
offer users a loaded footgun! :P
- makes File menu be part of the default menu — it's presence is
glaringly missing for regular development
- along with that, make the pieces of that menu available as lego pieces
to use - it can't just be `DefaultMainMenuContent`, all or nothing,
forcing downstream users to import everything from scratch
- finally, adds the Export menu as initially intended by this PR!

@steveruizok let's discuss if you have some notes on this and we can
talk about the shape of things here.

### Change Type

- [x] `patch` — Bug fix

### Release Notes

- Composable UI: makes File items be more granularly accessible / usable
- Menu: show Export under the File menu.
This commit is contained in:
Mime Čuvalo 2024-02-26 15:01:56 +00:00 committed by GitHub
parent f19b12c42e
commit fb852459db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 441 additions and 175 deletions

View file

@ -1,4 +1,5 @@
import { import {
ExportFileContentSubMenu,
TldrawUiMenuGroup, TldrawUiMenuGroup,
TldrawUiMenuItem, TldrawUiMenuItem,
TldrawUiMenuSubmenu, TldrawUiMenuSubmenu,
@ -17,9 +18,10 @@ export function LocalFileMenu() {
return ( return (
<TldrawUiMenuSubmenu id="file" label="menu.file"> <TldrawUiMenuSubmenu id="file" label="menu.file">
<TldrawUiMenuGroup id="file-actions"> <TldrawUiMenuGroup id="file-actions">
<TldrawUiMenuItem {...actions[SAVE_FILE_COPY_ACTION]} />
<TldrawUiMenuItem {...actions[OPEN_FILE_ACTION]} />
<TldrawUiMenuItem {...actions[NEW_PROJECT_ACTION]} /> <TldrawUiMenuItem {...actions[NEW_PROJECT_ACTION]} />
<TldrawUiMenuItem {...actions[OPEN_FILE_ACTION]} />
<TldrawUiMenuItem {...actions[SAVE_FILE_COPY_ACTION]} />
<ExportFileContentSubMenu />
</TldrawUiMenuGroup> </TldrawUiMenuGroup>
<TldrawUiMenuGroup id="share"> <TldrawUiMenuGroup id="share">
<TldrawUiMenuItem {...actions[SHARE_PROJECT_ACTION]} /> <TldrawUiMenuItem {...actions[SHARE_PROJECT_ACTION]} />
@ -35,6 +37,7 @@ export function MultiplayerFileMenu() {
<TldrawUiMenuSubmenu id="file" label="menu.file"> <TldrawUiMenuSubmenu id="file" label="menu.file">
<TldrawUiMenuGroup id="file-actions"> <TldrawUiMenuGroup id="file-actions">
<TldrawUiMenuItem {...actions[SAVE_FILE_COPY_ACTION]} /> <TldrawUiMenuItem {...actions[SAVE_FILE_COPY_ACTION]} />
<ExportFileContentSubMenu />
</TldrawUiMenuGroup> </TldrawUiMenuGroup>
<TldrawUiMenuGroup id="share"> <TldrawUiMenuGroup id="share">
<TldrawUiMenuItem {...actions[FORK_PROJECT_ACTION]} /> <TldrawUiMenuItem {...actions[FORK_PROJECT_ACTION]} />

View file

@ -6,12 +6,16 @@ import {
DefaultKeyboardShortcutsDialog, DefaultKeyboardShortcutsDialog,
DefaultKeyboardShortcutsDialogContent, DefaultKeyboardShortcutsDialogContent,
DefaultMainMenu, DefaultMainMenu,
DefaultMainMenuContent, EditSubmenu,
Editor, Editor,
ExtrasGroup,
ObjectSubmenu,
PreferencesGroup,
TLComponents, TLComponents,
Tldraw, Tldraw,
TldrawUiMenuGroup, TldrawUiMenuGroup,
TldrawUiMenuItem, TldrawUiMenuItem,
ViewSubmenu,
useActions, useActions,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { useCallback } from 'react' import { useCallback } from 'react'
@ -44,7 +48,11 @@ const components: TLComponents = {
MainMenu: () => ( MainMenu: () => (
<DefaultMainMenu> <DefaultMainMenu>
<LocalFileMenu /> <LocalFileMenu />
<DefaultMainMenuContent /> <EditSubmenu />
<ObjectSubmenu />
<ViewSubmenu />
<ExtrasGroup />
<PreferencesGroup />
<Links /> <Links />
</DefaultMainMenu> </DefaultMainMenu>
), ),

View file

@ -6,13 +6,17 @@ import {
DefaultKeyboardShortcutsDialog, DefaultKeyboardShortcutsDialog,
DefaultKeyboardShortcutsDialogContent, DefaultKeyboardShortcutsDialogContent,
DefaultMainMenu, DefaultMainMenu,
DefaultMainMenuContent, EditSubmenu,
Editor, Editor,
ExtrasGroup,
ObjectSubmenu,
OfflineIndicator, OfflineIndicator,
PreferencesGroup,
TLComponents, TLComponents,
Tldraw, Tldraw,
TldrawUiMenuGroup, TldrawUiMenuGroup,
TldrawUiMenuItem, TldrawUiMenuItem,
ViewSubmenu,
atom, atom,
debugFlags, debugFlags,
lns, lns,
@ -66,7 +70,11 @@ const components: TLComponents = {
MainMenu: () => ( MainMenu: () => (
<DefaultMainMenu> <DefaultMainMenu>
<MultiplayerFileMenu /> <MultiplayerFileMenu />
<DefaultMainMenuContent /> <EditSubmenu />
<ObjectSubmenu />
<ViewSubmenu />
<ExtrasGroup />
<PreferencesGroup />
<Links /> <Links />
</DefaultMainMenu> </DefaultMainMenu>
), ),

View file

@ -1,7 +1,6 @@
import { import {
TLUiDialogsContextType, TLUiDialogsContextType,
TldrawUiButton, TldrawUiButton,
TldrawUiButtonCheck,
TldrawUiButtonLabel, TldrawUiButtonLabel,
TldrawUiDialogBody, TldrawUiDialogBody,
TldrawUiDialogCloseButton, TldrawUiDialogCloseButton,
@ -10,11 +9,8 @@ import {
TldrawUiDialogTitle, TldrawUiDialogTitle,
useTranslation, useTranslation,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { useState } from 'react'
import { userPreferences } from './userPreferences'
export async function shouldClearDocument(addDialog: TLUiDialogsContextType['addDialog']) { export async function shouldClearDocument(addDialog: TLUiDialogsContextType['addDialog']) {
if (userPreferences.showFileClearWarning.get()) {
const shouldContinue = await new Promise<boolean>((resolve) => { const shouldContinue = await new Promise<boolean>((resolve) => {
addDialog({ addDialog({
component: ({ onClose }) => ( component: ({ onClose }) => (
@ -36,8 +32,6 @@ export async function shouldClearDocument(addDialog: TLUiDialogsContextType['add
}) })
return shouldContinue return shouldContinue
}
return true
} }
function ConfirmClearDialog({ function ConfirmClearDialog({
@ -48,7 +42,6 @@ function ConfirmClearDialog({
onContinue: () => void onContinue: () => void
}) { }) {
const msg = useTranslation() const msg = useTranslation()
const [dontShowAgain, setDontShowAgain] = useState(false)
return ( return (
<> <>
<TldrawUiDialogHeader> <TldrawUiDialogHeader>
@ -59,28 +52,10 @@ function ConfirmClearDialog({
{msg('file-system.confirm-clear.description')} {msg('file-system.confirm-clear.description')}
</TldrawUiDialogBody> </TldrawUiDialogBody>
<TldrawUiDialogFooter className="tlui-dialog__footer__actions"> <TldrawUiDialogFooter className="tlui-dialog__footer__actions">
<TldrawUiButton
type="normal"
onClick={() => setDontShowAgain(!dontShowAgain)}
style={{ marginRight: 'auto' }}
>
<TldrawUiButtonCheck checked={dontShowAgain} />
<TldrawUiButtonLabel>
{msg('file-system.confirm-clear.dont-show-again')}
</TldrawUiButtonLabel>
</TldrawUiButton>
<TldrawUiButton type="normal" onClick={onCancel}> <TldrawUiButton type="normal" onClick={onCancel}>
<TldrawUiButtonLabel>{msg('file-system.confirm-clear.cancel')}</TldrawUiButtonLabel> <TldrawUiButtonLabel>{msg('file-system.confirm-clear.cancel')}</TldrawUiButtonLabel>
</TldrawUiButton> </TldrawUiButton>
<TldrawUiButton <TldrawUiButton type="primary" onClick={async () => onContinue()}>
type="primary"
onClick={async () => {
if (dontShowAgain) {
userPreferences.showFileClearWarning.set(false)
}
onContinue()
}}
>
<TldrawUiButtonLabel>{msg('file-system.confirm-clear.continue')}</TldrawUiButtonLabel> <TldrawUiButtonLabel>{msg('file-system.confirm-clear.continue')}</TldrawUiButtonLabel>
</TldrawUiButton> </TldrawUiButton>
</TldrawUiDialogFooter> </TldrawUiDialogFooter>

View file

@ -1,7 +1,6 @@
import { import {
TLUiDialogsContextType, TLUiDialogsContextType,
TldrawUiButton, TldrawUiButton,
TldrawUiButtonCheck,
TldrawUiButtonLabel, TldrawUiButtonLabel,
TldrawUiDialogBody, TldrawUiDialogBody,
TldrawUiDialogCloseButton, TldrawUiDialogCloseButton,
@ -10,11 +9,9 @@ import {
TldrawUiDialogTitle, TldrawUiDialogTitle,
useTranslation, useTranslation,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { useState } from 'react'
import { userPreferences } from './userPreferences'
/** @public */
export async function shouldOverrideDocument(addDialog: TLUiDialogsContextType['addDialog']) { export async function shouldOverrideDocument(addDialog: TLUiDialogsContextType['addDialog']) {
if (userPreferences.showFileOpenWarning.get()) {
const shouldContinue = await new Promise<boolean>((resolve) => { const shouldContinue = await new Promise<boolean>((resolve) => {
addDialog({ addDialog({
component: ({ onClose }) => ( component: ({ onClose }) => (
@ -36,8 +33,6 @@ export async function shouldOverrideDocument(addDialog: TLUiDialogsContextType['
}) })
return shouldContinue return shouldContinue
}
return true
} }
function ConfirmOpenDialog({ function ConfirmOpenDialog({
@ -48,7 +43,6 @@ function ConfirmOpenDialog({
onContinue: () => void onContinue: () => void
}) { }) {
const msg = useTranslation() const msg = useTranslation()
const [dontShowAgain, setDontShowAgain] = useState(false)
return ( return (
<> <>
<TldrawUiDialogHeader> <TldrawUiDialogHeader>
@ -59,28 +53,10 @@ function ConfirmOpenDialog({
{msg('file-system.confirm-open.description')} {msg('file-system.confirm-open.description')}
</TldrawUiDialogBody> </TldrawUiDialogBody>
<TldrawUiDialogFooter className="tlui-dialog__footer__actions"> <TldrawUiDialogFooter className="tlui-dialog__footer__actions">
<TldrawUiButton
type="normal"
onClick={() => setDontShowAgain(!dontShowAgain)}
style={{ marginRight: 'auto' }}
>
<TldrawUiButtonCheck checked={dontShowAgain} />
<TldrawUiButtonLabel>
{msg('file-system.confirm-open.dont-show-again')}
</TldrawUiButtonLabel>
</TldrawUiButton>
<TldrawUiButton type="normal" onClick={onCancel}> <TldrawUiButton type="normal" onClick={onCancel}>
<TldrawUiButtonLabel>{msg('file-system.confirm-open.cancel')}</TldrawUiButtonLabel> <TldrawUiButtonLabel>{msg('file-system.confirm-open.cancel')}</TldrawUiButtonLabel>
</TldrawUiButton> </TldrawUiButton>
<TldrawUiButton <TldrawUiButton type="primary" onClick={async () => onContinue()}>
type="primary"
onClick={async () => {
if (dontShowAgain) {
userPreferences.showFileOpenWarning.set(false)
}
onContinue()
}}
>
<TldrawUiButtonLabel>{msg('file-system.confirm-open.open')}</TldrawUiButtonLabel> <TldrawUiButtonLabel>{msg('file-system.confirm-open.open')}</TldrawUiButtonLabel>
</TldrawUiButton> </TldrawUiButton>
</TldrawUiDialogFooter> </TldrawUiDialogFooter>

View file

@ -215,6 +215,7 @@
"menu.title": "Menu", "menu.title": "Menu",
"menu.copy-as": "Copy as", "menu.copy-as": "Copy as",
"menu.edit": "Edit", "menu.edit": "Edit",
"menu.object": "Object",
"menu.export-as": "Export as", "menu.export-as": "Export as",
"menu.file": "File", "menu.file": "File",
"menu.language": "Language", "menu.language": "Language",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -180,7 +180,15 @@ export {
DefaultMainMenu, DefaultMainMenu,
type TLUiMainMenuProps, type TLUiMainMenuProps,
} from './lib/ui/components/MainMenu/DefaultMainMenu' } from './lib/ui/components/MainMenu/DefaultMainMenu'
export { DefaultMainMenuContent } from './lib/ui/components/MainMenu/DefaultMainMenuContent' export {
DefaultMainMenuContent,
EditSubmenu,
ExportFileContentSubMenu,
ExtrasGroup,
ObjectSubmenu,
PreferencesGroup,
ViewSubmenu,
} from './lib/ui/components/MainMenu/DefaultMainMenuContent'
export { export {
DefaultQuickActions, DefaultQuickActions,

View file

@ -3,8 +3,6 @@ import {
ArrangeMenuSubmenu, ArrangeMenuSubmenu,
ClipboardMenuGroup, ClipboardMenuGroup,
ConversionsMenuGroup, ConversionsMenuGroup,
DeleteGroup,
DuplicateMenuItem,
EditLinkMenuItem, EditLinkMenuItem,
EmbedsGroup, EmbedsGroup,
FitFrameToContentMenuItem, FitFrameToContentMenuItem,
@ -36,7 +34,6 @@ export function DefaultContextMenuContent() {
<TldrawUiMenuGroup id="selection"> <TldrawUiMenuGroup id="selection">
<ToggleAutoSizeMenuItem /> <ToggleAutoSizeMenuItem />
<EditLinkMenuItem /> <EditLinkMenuItem />
<DuplicateMenuItem />
<GroupMenuItem /> <GroupMenuItem />
<UngroupMenuItem /> <UngroupMenuItem />
<RemoveFrameMenuItem /> <RemoveFrameMenuItem />
@ -52,7 +49,6 @@ export function DefaultContextMenuContent() {
<ClipboardMenuGroup /> <ClipboardMenuGroup />
<ConversionsMenuGroup /> <ConversionsMenuGroup />
<SetSelectionGroup /> <SetSelectionGroup />
<DeleteGroup />
</> </>
) )
} }

View file

@ -5,8 +5,6 @@ import { LanguageMenu } from '../LanguageMenu'
import { import {
ClipboardMenuGroup, ClipboardMenuGroup,
ConversionsMenuGroup, ConversionsMenuGroup,
DeleteGroup,
DuplicateMenuItem,
EditLinkMenuItem, EditLinkMenuItem,
EmbedsGroup, EmbedsGroup,
FitFrameToContentMenuItem, FitFrameToContentMenuItem,
@ -23,6 +21,7 @@ import {
ToggleReduceMotionItem, ToggleReduceMotionItem,
ToggleSnapModeItem, ToggleSnapModeItem,
ToggleToolLockItem, ToggleToolLockItem,
ToggleTransparentBgMenuItem,
UngroupMenuItem, UngroupMenuItem,
UnlockAllMenuItem, UnlockAllMenuItem,
ZoomTo100MenuItem, ZoomTo100MenuItem,
@ -38,6 +37,7 @@ export function DefaultMainMenuContent() {
return ( return (
<> <>
<EditSubmenu /> <EditSubmenu />
<ObjectSubmenu />
<ViewSubmenu /> <ViewSubmenu />
<ExtrasGroup /> <ExtrasGroup />
<PreferencesGroup /> <PreferencesGroup />
@ -45,7 +45,26 @@ export function DefaultMainMenuContent() {
) )
} }
function EditSubmenu() { /** @public */
export function ExportFileContentSubMenu() {
const actions = useActions()
return (
<TldrawUiMenuSubmenu id="export-as" label="context-menu.export-as" size="small">
<TldrawUiMenuGroup id="export-as-group">
<TldrawUiMenuItem {...actions['export-as-svg']} />
<TldrawUiMenuItem {...actions['export-as-png']} />
<TldrawUiMenuItem {...actions['export-as-json']} />
</TldrawUiMenuGroup>
<TldrawUiMenuGroup id="export-as-bg">
<ToggleTransparentBgMenuItem />
</TldrawUiMenuGroup>
</TldrawUiMenuSubmenu>
)
}
/** @public */
export function EditSubmenu() {
const editor = useEditor() const editor = useEditor()
const selectToolActive = useValue( const selectToolActive = useValue(
@ -54,38 +73,70 @@ function EditSubmenu() {
[editor] [editor]
) )
if (!selectToolActive) return null
return ( return (
<TldrawUiMenuSubmenu id="edit" label="menu.edit"> <TldrawUiMenuSubmenu id="edit" label="menu.edit" disabled={!selectToolActive}>
<UndoRedoGroup /> <UndoRedoGroup />
<ClipboardMenuGroup /> <ClipboardMenuGroup />
<ConversionsMenuGroup />
<SetSelectionGroup /> <SetSelectionGroup />
<SelectionMenuGroup />
<EmbedsGroup />
<DeleteGroup />
</TldrawUiMenuSubmenu> </TldrawUiMenuSubmenu>
) )
} }
function SelectionMenuGroup() { /** @public */
export function ObjectSubmenu() {
const editor = useEditor()
const selectToolActive = useValue(
'isSelectToolActive',
() => editor.getCurrentToolId() === 'select',
[editor]
)
return ( return (
<TldrawUiMenuGroup id="selection"> <TldrawUiMenuSubmenu id="object" label="menu.object" disabled={!selectToolActive}>
<ConversionsMenuGroup />
<MultiShapeMenuGroup />
<MiscMenuGroup />
<EmbedsGroup />
<LockGroup />
</TldrawUiMenuSubmenu>
)
}
/** @public */
export function MiscMenuGroup() {
return (
<TldrawUiMenuGroup id="misc">
<ToggleAutoSizeMenuItem /> <ToggleAutoSizeMenuItem />
<EditLinkMenuItem /> <EditLinkMenuItem />
<DuplicateMenuItem /> </TldrawUiMenuGroup>
<GroupMenuItem /> )
<UngroupMenuItem /> }
<RemoveFrameMenuItem />
<FitFrameToContentMenuItem /> /** @public */
export function LockGroup() {
return (
<TldrawUiMenuGroup id="lock">
<ToggleLockMenuItem /> <ToggleLockMenuItem />
<UnlockAllMenuItem /> <UnlockAllMenuItem />
</TldrawUiMenuGroup> </TldrawUiMenuGroup>
) )
} }
function UndoRedoGroup() { /** @public */
export function MultiShapeMenuGroup() {
return (
<TldrawUiMenuGroup id="multi-shape">
<GroupMenuItem />
<UngroupMenuItem />
<RemoveFrameMenuItem />
<FitFrameToContentMenuItem />
</TldrawUiMenuGroup>
)
}
/** @public */
export function UndoRedoGroup() {
const actions = useActions() const actions = useActions()
const canUndo = useCanUndo() const canUndo = useCanUndo()
const canRedo = useCanRedo() const canRedo = useCanRedo()
@ -97,7 +148,8 @@ function UndoRedoGroup() {
) )
} }
function ViewSubmenu() { /** @public */
export function ViewSubmenu() {
const actions = useActions() const actions = useActions()
return ( return (
<TldrawUiMenuSubmenu id="view" label="menu.view"> <TldrawUiMenuSubmenu id="view" label="menu.view">
@ -112,7 +164,8 @@ function ViewSubmenu() {
) )
} }
function ExtrasGroup() { /** @public */
export function ExtrasGroup() {
const actions = useActions() const actions = useActions()
return ( return (
<TldrawUiMenuGroup id="extras"> <TldrawUiMenuGroup id="extras">
@ -124,7 +177,8 @@ function ExtrasGroup() {
/* ------------------- Preferences ------------------ */ /* ------------------- Preferences ------------------ */
function PreferencesGroup() { /** @public */
export function PreferencesGroup() {
return ( return (
<TldrawUiMenuGroup id="preferences"> <TldrawUiMenuGroup id="preferences">
<TldrawUiMenuSubmenu id="preferences" label="menu.preferences"> <TldrawUiMenuSubmenu id="preferences" label="menu.preferences">

View file

@ -31,36 +31,36 @@ import { TldrawUiMenuSubmenu } from './primitives/menus/TldrawUiMenuSubmenu'
export function ToggleAutoSizeMenuItem() { export function ToggleAutoSizeMenuItem() {
const actions = useActions() const actions = useActions()
const shouldDisplay = useShowAutoSizeToggle() const shouldDisplay = useShowAutoSizeToggle()
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['toggle-auto-size']} /> return <TldrawUiMenuItem {...actions['toggle-auto-size']} disabled={!shouldDisplay} />
} }
export function EditLinkMenuItem() { export function EditLinkMenuItem() {
const actions = useActions() const actions = useActions()
const shouldDisplay = useHasLinkShapeSelected() const shouldDisplay = useHasLinkShapeSelected()
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['edit-link']} /> return <TldrawUiMenuItem {...actions['edit-link']} disabled={!shouldDisplay} />
} }
export function DuplicateMenuItem() { export function DuplicateMenuItem() {
const actions = useActions() const actions = useActions()
const shouldDisplay = useUnlockedSelectedShapesCount(1) const shouldDisplay = useUnlockedSelectedShapesCount(1)
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['duplicate']} /> return <TldrawUiMenuItem {...actions['duplicate']} disabled={!shouldDisplay} />
} }
export function GroupMenuItem() { export function GroupMenuItem() {
const actions = useActions() const actions = useActions()
const shouldDisplay = useAllowGroup() const shouldDisplay = useAllowGroup()
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['group']} /> return <TldrawUiMenuItem {...actions['group']} disabled={!shouldDisplay} />
} }
export function UngroupMenuItem() { export function UngroupMenuItem() {
const actions = useActions() const actions = useActions()
const shouldDisplay = useAllowUngroup() const shouldDisplay = useAllowUngroup()
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['ungroup']} /> return <TldrawUiMenuItem {...actions['ungroup']} disabled={!shouldDisplay} />
} }
export function RemoveFrameMenuItem() { export function RemoveFrameMenuItem() {
@ -75,8 +75,8 @@ export function RemoveFrameMenuItem() {
}, },
[editor] [editor]
) )
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['remove-frame']} /> return <TldrawUiMenuItem {...actions['remove-frame']} disabled={!shouldDisplay} />
} }
export function FitFrameToContentMenuItem() { export function FitFrameToContentMenuItem() {
@ -94,8 +94,8 @@ export function FitFrameToContentMenuItem() {
}, },
[editor] [editor]
) )
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['fit-frame-to-content']} /> return <TldrawUiMenuItem {...actions['fit-frame-to-content']} disabled={!shouldDisplay} />
} }
export function ToggleLockMenuItem() { export function ToggleLockMenuItem() {
@ -104,8 +104,8 @@ export function ToggleLockMenuItem() {
const shouldDisplay = useValue('selected shapes', () => editor.getSelectedShapes().length > 0, [ const shouldDisplay = useValue('selected shapes', () => editor.getSelectedShapes().length > 0, [
editor, editor,
]) ])
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['toggle-lock']} /> return <TldrawUiMenuItem {...actions['toggle-lock']} disabled={!shouldDisplay} />
} }
export function ToggleTransparentBgMenuItem() { export function ToggleTransparentBgMenuItem() {
@ -125,8 +125,8 @@ export function UnlockAllMenuItem() {
const shouldDisplay = useValue('any shapes', () => editor.getCurrentPageShapeIds().size > 0, [ const shouldDisplay = useValue('any shapes', () => editor.getCurrentPageShapeIds().size > 0, [
editor, editor,
]) ])
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['unlock-all']} /> return <TldrawUiMenuItem {...actions['unlock-all']} disabled={!shouldDisplay} />
} }
/* ---------------------- Zoom ---------------------- */ /* ---------------------- Zoom ---------------------- */
@ -174,11 +174,38 @@ export function ZoomToSelectionMenuItem() {
/* -------------------- Clipboard ------------------- */ /* -------------------- Clipboard ------------------- */
export function ClipboardMenuGroup() { export function ClipboardMenuGroup() {
const editor = useEditor()
const actions = useActions()
const atLeastOneShapeOnPage = useValue(
'atLeastOneShapeOnPage',
() => editor.getCurrentPageShapeIds().size > 0,
[]
)
return ( return (
<TldrawUiMenuGroup id="clipboard"> <TldrawUiMenuGroup id="clipboard">
<CutMenuItem /> <CutMenuItem />
<CopyMenuItem /> <CopyMenuItem />
<TldrawUiMenuSubmenu
id="copy-as"
label="context-menu.copy-as"
size="small"
disabled={!atLeastOneShapeOnPage}
>
<TldrawUiMenuGroup id="copy-as-group">
<TldrawUiMenuItem {...actions['copy-as-svg']} />
{Boolean(window.navigator.clipboard?.write) && (
<TldrawUiMenuItem {...actions['copy-as-png']} />
)}
<TldrawUiMenuItem {...actions['copy-as-json']} />
</TldrawUiMenuGroup>
<TldrawUiMenuGroup id="copy-as-bg">
<ToggleTransparentBgMenuItem />
</TldrawUiMenuGroup>
</TldrawUiMenuSubmenu>
<DuplicateMenuItem />
<PasteMenuItem /> <PasteMenuItem />
<DeleteMenuItem />
</TldrawUiMenuGroup> </TldrawUiMenuGroup>
) )
} }
@ -186,22 +213,22 @@ export function ClipboardMenuGroup() {
export function CutMenuItem() { export function CutMenuItem() {
const actions = useActions() const actions = useActions()
const shouldDisplay = useUnlockedSelectedShapesCount(1) const shouldDisplay = useUnlockedSelectedShapesCount(1)
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['cut']} /> return <TldrawUiMenuItem {...actions['cut']} disabled={!shouldDisplay} />
} }
export function CopyMenuItem() { export function CopyMenuItem() {
const actions = useActions() const actions = useActions()
const shouldDisplay = useAnySelectedShapesCount(1) const shouldDisplay = useAnySelectedShapesCount(1)
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['copy']} /> return <TldrawUiMenuItem {...actions['copy']} disabled={!shouldDisplay} />
} }
export function PasteMenuItem() { export function PasteMenuItem() {
const actions = useActions() const actions = useActions()
const shouldDisplay = showMenuPaste const shouldDisplay = showMenuPaste
if (!shouldDisplay) return null
return <TldrawUiMenuItem {...actions['paste']} /> return <TldrawUiMenuItem {...actions['paste']} disabled={!shouldDisplay} />
} }
/* ------------------- Conversions ------------------ */ /* ------------------- Conversions ------------------ */
@ -214,23 +241,15 @@ export function ConversionsMenuGroup() {
() => editor.getCurrentPageShapeIds().size > 0, () => editor.getCurrentPageShapeIds().size > 0,
[] []
) )
if (!atLeastOneShapeOnPage) return null
return ( return (
<TldrawUiMenuGroup id="conversions"> <TldrawUiMenuGroup id="conversions">
<TldrawUiMenuSubmenu id="copy-as" label="context-menu.copy-as" size="small"> <TldrawUiMenuSubmenu
<TldrawUiMenuGroup id="copy-as-group"> id="export-as"
<TldrawUiMenuItem {...actions['copy-as-svg']} /> label="context-menu.export-as"
{Boolean(window.navigator.clipboard?.write) && ( size="small"
<TldrawUiMenuItem {...actions['copy-as-png']} /> disabled={!atLeastOneShapeOnPage}
)} >
<TldrawUiMenuItem {...actions['copy-as-json']} />
</TldrawUiMenuGroup>
<TldrawUiMenuGroup id="copy-as-bg">
<ToggleTransparentBgMenuItem />
</TldrawUiMenuGroup>
</TldrawUiMenuSubmenu>
<TldrawUiMenuSubmenu id="export-as" label="context-menu.export-as" size="small">
<TldrawUiMenuGroup id="export-as-group"> <TldrawUiMenuGroup id="export-as-group">
<TldrawUiMenuItem {...actions['export-as-svg']} /> <TldrawUiMenuItem {...actions['export-as-svg']} />
<TldrawUiMenuItem {...actions['export-as-png']} /> <TldrawUiMenuItem {...actions['export-as-png']} />
@ -254,25 +273,21 @@ export function SetSelectionGroup() {
() => editor.getCurrentPageShapeIds().size > 0, () => editor.getCurrentPageShapeIds().size > 0,
[editor] [editor]
) )
if (!atLeastOneShapeOnPage) return null
return ( return (
<TldrawUiMenuGroup id="set-selection-group"> <TldrawUiMenuGroup id="set-selection-group">
<TldrawUiMenuItem {...actions['select-all']} /> <TldrawUiMenuItem {...actions['select-all']} disabled={!atLeastOneShapeOnPage} />
</TldrawUiMenuGroup> </TldrawUiMenuGroup>
) )
} }
/* ------------------ Delete Group ------------------ */ /* ------------------ Delete Group ------------------ */
export function DeleteGroup() { export function DeleteMenuItem() {
const actions = useActions() const actions = useActions()
const oneSelected = useUnlockedSelectedShapesCount(1) const oneSelected = useUnlockedSelectedShapesCount(1)
if (!oneSelected) return null
return ( return <TldrawUiMenuItem {...actions['delete']} disabled={!oneSelected} />
<TldrawUiMenuGroup id="delete-group">
<TldrawUiMenuItem {...actions['delete']} />
</TldrawUiMenuGroup>
)
} }
/* --------------------- Modify --------------------- */ /* --------------------- Modify --------------------- */
@ -449,9 +464,13 @@ export function EmbedsGroup() {
return ( return (
<TldrawUiMenuGroup id="embeds"> <TldrawUiMenuGroup id="embeds">
{oneEmbedSelected && <TldrawUiMenuItem {...actions['edit-embed']} />} {/* XXX this doesn't exist?? */}
{oneEmbedSelected && <TldrawUiMenuItem {...actions['convert-to-bookmark']} />} {/* <TldrawUiMenuItem {...actions['edit-embed']} disabled={!oneEmbedSelected} /> */}
{oneEmbeddableBookmarkSelected && <TldrawUiMenuItem {...actions['convert-to-embed']} />} <TldrawUiMenuItem {...actions['convert-to-bookmark']} disabled={!oneEmbedSelected} />
<TldrawUiMenuItem
{...actions['convert-to-embed']}
disabled={!oneEmbeddableBookmarkSelected}
/>
</TldrawUiMenuGroup> </TldrawUiMenuGroup>
) )
} }

View file

@ -120,7 +120,7 @@ export function TldrawUiDropdownMenuSubTrigger({
disabled, disabled,
}: TLUiDropdownMenuSubTriggerProps) { }: TLUiDropdownMenuSubTriggerProps) {
return ( return (
<_DropdownMenu.SubTrigger dir="ltr" asChild> <_DropdownMenu.SubTrigger dir="ltr" asChild disabled={disabled}>
<TldrawUiButton <TldrawUiButton
type="menu" type="menu"
className="tlui-menu__submenu__trigger" className="tlui-menu__submenu__trigger"

View file

@ -219,6 +219,7 @@ export type TLUiTranslationKey =
| 'menu.title' | 'menu.title'
| 'menu.copy-as' | 'menu.copy-as'
| 'menu.edit' | 'menu.edit'
| 'menu.object'
| 'menu.export-as' | 'menu.export-as'
| 'menu.file' | 'menu.file'
| 'menu.language' | 'menu.language'

View file

@ -219,6 +219,7 @@ export const DEFAULT_TRANSLATION = {
'menu.title': 'Menu', 'menu.title': 'Menu',
'menu.copy-as': 'Copy as', 'menu.copy-as': 'Copy as',
'menu.edit': 'Edit', 'menu.edit': 'Edit',
'menu.object': 'Object',
'menu.export-as': 'Export as', 'menu.export-as': 'Export as',
'menu.file': 'File', 'menu.file': 'File',
'menu.language': 'Language', 'menu.language': 'Language',