Extract Vue SDK commands and menu model
This commit is contained in:
parent
b9b960f6ef
commit
0c30d044b2
230
packages/vue/src/commands/useEditorCommands.ts
Normal file
230
packages/vue/src/commands/useEditorCommands.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
import { computed } from 'vue'
|
||||
|
||||
import { useEditor } from '../shared/editorContext'
|
||||
import { usePageList } from '../shared/usePageList'
|
||||
import { useSelectionCapabilities } from '../selection/useSelectionCapabilities'
|
||||
import { useSelectionState } from '../shared/useSelectionState'
|
||||
|
||||
import type { Component, ComputedRef } from 'vue'
|
||||
|
||||
export type EditorCommandId =
|
||||
| 'edit.undo'
|
||||
| 'edit.redo'
|
||||
| 'selection.selectAll'
|
||||
| 'selection.duplicate'
|
||||
| 'selection.delete'
|
||||
| 'selection.group'
|
||||
| 'selection.ungroup'
|
||||
| 'selection.createComponent'
|
||||
| 'selection.createComponentSet'
|
||||
| 'selection.createInstance'
|
||||
| 'selection.detachInstance'
|
||||
| 'selection.goToMainComponent'
|
||||
| 'selection.wrapInAutoLayout'
|
||||
| 'selection.bringToFront'
|
||||
| 'selection.sendToBack'
|
||||
| 'selection.toggleVisibility'
|
||||
| 'selection.toggleLock'
|
||||
| 'selection.moveToPage'
|
||||
| 'view.zoom100'
|
||||
| 'view.zoomFit'
|
||||
| 'view.zoomSelection'
|
||||
|
||||
export interface EditorCommand {
|
||||
id: EditorCommandId
|
||||
label: string
|
||||
enabled: ComputedRef<boolean>
|
||||
run: () => void
|
||||
}
|
||||
|
||||
export interface EditorCommandMenuItem {
|
||||
label: string
|
||||
shortcut?: string
|
||||
action?: () => void
|
||||
disabled?: boolean
|
||||
checked?: boolean
|
||||
onCheckedChange?: (checked: boolean) => void
|
||||
icon?: Component
|
||||
}
|
||||
|
||||
export interface EditorCommandMenuSeparator {
|
||||
separator: true
|
||||
}
|
||||
|
||||
export type EditorCommandMenuEntry = EditorCommandMenuItem | EditorCommandMenuSeparator
|
||||
|
||||
export function useEditorCommands() {
|
||||
const editor = useEditor()
|
||||
const selection = useSelectionState()
|
||||
const capabilities = useSelectionCapabilities()
|
||||
const { pages } = usePageList()
|
||||
|
||||
const commands: Record<EditorCommandId, EditorCommand> = {
|
||||
'edit.undo': {
|
||||
id: 'edit.undo',
|
||||
label: 'Undo',
|
||||
enabled: capabilities.canUndo,
|
||||
run: () => editor.undoAction()
|
||||
},
|
||||
'edit.redo': {
|
||||
id: 'edit.redo',
|
||||
label: 'Redo',
|
||||
enabled: capabilities.canRedo,
|
||||
run: () => editor.redoAction()
|
||||
},
|
||||
'selection.selectAll': {
|
||||
id: 'selection.selectAll',
|
||||
label: 'Select all',
|
||||
enabled: capabilities.canSelectAll,
|
||||
run: () => editor.selectAll()
|
||||
},
|
||||
'selection.duplicate': {
|
||||
id: 'selection.duplicate',
|
||||
label: 'Duplicate',
|
||||
enabled: capabilities.canDuplicate,
|
||||
run: () => editor.duplicateSelected()
|
||||
},
|
||||
'selection.delete': {
|
||||
id: 'selection.delete',
|
||||
label: 'Delete',
|
||||
enabled: capabilities.canDelete,
|
||||
run: () => editor.deleteSelected()
|
||||
},
|
||||
'selection.group': {
|
||||
id: 'selection.group',
|
||||
label: 'Group',
|
||||
enabled: capabilities.canGroup,
|
||||
run: () => editor.groupSelected()
|
||||
},
|
||||
'selection.ungroup': {
|
||||
id: 'selection.ungroup',
|
||||
label: 'Ungroup',
|
||||
enabled: capabilities.canUngroup,
|
||||
run: () => editor.ungroupSelected()
|
||||
},
|
||||
'selection.createComponent': {
|
||||
id: 'selection.createComponent',
|
||||
label: 'Create component',
|
||||
enabled: capabilities.canCreateComponent,
|
||||
run: () => editor.createComponentFromSelection()
|
||||
},
|
||||
'selection.createComponentSet': {
|
||||
id: 'selection.createComponentSet',
|
||||
label: 'Create component set',
|
||||
enabled: capabilities.canCreateComponentSet,
|
||||
run: () => editor.createComponentSetFromComponents()
|
||||
},
|
||||
'selection.createInstance': {
|
||||
id: 'selection.createInstance',
|
||||
label: 'Create instance',
|
||||
enabled: capabilities.canCreateInstance,
|
||||
run: () => {
|
||||
const node = selection.selectedNode.value
|
||||
if (node?.type === 'COMPONENT') editor.createInstanceFromComponent(node.id)
|
||||
}
|
||||
},
|
||||
'selection.detachInstance': {
|
||||
id: 'selection.detachInstance',
|
||||
label: 'Detach instance',
|
||||
enabled: capabilities.canDetachInstance,
|
||||
run: () => editor.detachInstance()
|
||||
},
|
||||
'selection.goToMainComponent': {
|
||||
id: 'selection.goToMainComponent',
|
||||
label: 'Go to main component',
|
||||
enabled: capabilities.canGoToMainComponent,
|
||||
run: () => editor.goToMainComponent()
|
||||
},
|
||||
'selection.wrapInAutoLayout': {
|
||||
id: 'selection.wrapInAutoLayout',
|
||||
label: 'Add auto layout',
|
||||
enabled: capabilities.canWrapInAutoLayout,
|
||||
run: () => editor.wrapInAutoLayout()
|
||||
},
|
||||
'selection.bringToFront': {
|
||||
id: 'selection.bringToFront',
|
||||
label: 'Bring to front',
|
||||
enabled: capabilities.canBringToFront,
|
||||
run: () => editor.bringToFront()
|
||||
},
|
||||
'selection.sendToBack': {
|
||||
id: 'selection.sendToBack',
|
||||
label: 'Send to back',
|
||||
enabled: capabilities.canSendToBack,
|
||||
run: () => editor.sendToBack()
|
||||
},
|
||||
'selection.toggleVisibility': {
|
||||
id: 'selection.toggleVisibility',
|
||||
label: 'Toggle visibility',
|
||||
enabled: capabilities.canToggleVisibility,
|
||||
run: () => editor.toggleVisibility()
|
||||
},
|
||||
'selection.toggleLock': {
|
||||
id: 'selection.toggleLock',
|
||||
label: 'Toggle lock',
|
||||
enabled: capabilities.canToggleLock,
|
||||
run: () => editor.toggleLock()
|
||||
},
|
||||
'selection.moveToPage': {
|
||||
id: 'selection.moveToPage',
|
||||
label: 'Move to page',
|
||||
enabled: capabilities.canMoveToPage,
|
||||
run: () => {}
|
||||
},
|
||||
'view.zoom100': {
|
||||
id: 'view.zoom100',
|
||||
label: 'Zoom to 100%',
|
||||
enabled: computed(() => true),
|
||||
run: () => editor.zoomTo100()
|
||||
},
|
||||
'view.zoomFit': {
|
||||
id: 'view.zoomFit',
|
||||
label: 'Zoom to fit',
|
||||
enabled: computed(() => true),
|
||||
run: () => editor.zoomToFit()
|
||||
},
|
||||
'view.zoomSelection': {
|
||||
id: 'view.zoomSelection',
|
||||
label: 'Zoom to selection',
|
||||
enabled: capabilities.canZoomToSelection,
|
||||
run: () => editor.zoomToSelection()
|
||||
}
|
||||
}
|
||||
|
||||
const otherPages = computed(() => pages.value.filter((page) => page.id !== editor.state.currentPageId))
|
||||
|
||||
function getCommand(id: EditorCommandId) {
|
||||
return commands[id]
|
||||
}
|
||||
|
||||
function runCommand(id: EditorCommandId) {
|
||||
const command = commands[id]
|
||||
if (command.enabled.value) command.run()
|
||||
}
|
||||
|
||||
function moveSelectionToPage(pageId: string) {
|
||||
if (!capabilities.canMoveToPage.value) return
|
||||
editor.moveToPage(pageId)
|
||||
}
|
||||
|
||||
function menuItem(id: EditorCommandId, shortcut?: string): EditorCommandMenuItem {
|
||||
const command = getCommand(id)
|
||||
return {
|
||||
label: command.label,
|
||||
shortcut,
|
||||
get disabled() {
|
||||
return !command.enabled.value
|
||||
},
|
||||
action: () => runCommand(id)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
commands,
|
||||
otherPages,
|
||||
getCommand,
|
||||
runCommand,
|
||||
moveSelectionToPage,
|
||||
menuItem
|
||||
}
|
||||
}
|
||||
120
packages/vue/src/commands/useMenuModel.ts
Normal file
120
packages/vue/src/commands/useMenuModel.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { computed } from 'vue'
|
||||
|
||||
import { useEditorCommands } from './useEditorCommands'
|
||||
import { useSelectionState } from '../shared/useSelectionState'
|
||||
import { useEditor } from '../shared/editorContext'
|
||||
|
||||
export interface MenuActionNode {
|
||||
separator?: false
|
||||
label: string
|
||||
shortcut?: string
|
||||
action?: () => void
|
||||
disabled?: boolean
|
||||
checked?: boolean
|
||||
onCheckedChange?: (checked: boolean) => void
|
||||
sub?: MenuEntry[]
|
||||
}
|
||||
|
||||
export interface MenuSeparatorNode {
|
||||
separator: true
|
||||
}
|
||||
|
||||
export type MenuEntry = MenuActionNode | MenuSeparatorNode
|
||||
|
||||
export function useMenuModel() {
|
||||
const editor = useEditor()
|
||||
const { menuItem: commandMenuItem, otherPages, moveSelectionToPage } = useEditorCommands()
|
||||
const { hasSelection, isGroup, isInstance, isComponent, canCreateComponentSet, selectedNode } =
|
||||
useSelectionState()
|
||||
|
||||
const editMenu = computed<MenuEntry[]>(() => [
|
||||
commandMenuItem('edit.undo'),
|
||||
commandMenuItem('edit.redo'),
|
||||
{ separator: true },
|
||||
commandMenuItem('selection.duplicate'),
|
||||
commandMenuItem('selection.delete'),
|
||||
{ separator: true },
|
||||
commandMenuItem('selection.selectAll')
|
||||
])
|
||||
|
||||
const viewMenu = computed<MenuEntry[]>(() => [
|
||||
commandMenuItem('view.zoom100'),
|
||||
commandMenuItem('view.zoomFit'),
|
||||
commandMenuItem('view.zoomSelection')
|
||||
])
|
||||
|
||||
const objectMenu = computed<MenuEntry[]>(() => [
|
||||
commandMenuItem('selection.group'),
|
||||
commandMenuItem('selection.ungroup'),
|
||||
{ separator: true },
|
||||
commandMenuItem('selection.createComponent'),
|
||||
commandMenuItem('selection.createComponentSet'),
|
||||
commandMenuItem('selection.detachInstance'),
|
||||
{ separator: true },
|
||||
commandMenuItem('selection.bringToFront'),
|
||||
commandMenuItem('selection.sendToBack')
|
||||
])
|
||||
|
||||
const arrangeMenu = computed<MenuEntry[]>(() => [commandMenuItem('selection.wrapInAutoLayout')])
|
||||
|
||||
const appMenu = computed(() => [
|
||||
{ label: 'Edit', items: editMenu.value },
|
||||
{ label: 'View', items: viewMenu.value },
|
||||
{ label: 'Object', items: objectMenu.value },
|
||||
{ label: 'Arrange', items: arrangeMenu.value }
|
||||
])
|
||||
|
||||
const canvasMenu = computed<MenuEntry[]>(() => {
|
||||
const moveToPageSubmenu: MenuEntry[] = otherPages.value.map((page) => ({
|
||||
label: page.name,
|
||||
action: () => moveSelectionToPage(page.id)
|
||||
}))
|
||||
|
||||
return [
|
||||
{ ...commandMenuItem('selection.duplicate', '⌘D'), label: 'Duplicate' },
|
||||
commandMenuItem('selection.delete', '⌫'),
|
||||
{ separator: true },
|
||||
...(moveToPageSubmenu.length > 0 && hasSelection.value
|
||||
? [{ label: 'Move to page', sub: moveToPageSubmenu } satisfies MenuActionNode]
|
||||
: []),
|
||||
commandMenuItem('selection.bringToFront', ']'),
|
||||
commandMenuItem('selection.sendToBack', '['),
|
||||
{ separator: true },
|
||||
commandMenuItem('selection.group', '⌘G'),
|
||||
...(isGroup.value ? [commandMenuItem('selection.ungroup', '⇧⌘G')] : []),
|
||||
...(hasSelection.value ? [commandMenuItem('selection.wrapInAutoLayout', '⇧A')] : []),
|
||||
{ separator: true },
|
||||
commandMenuItem('selection.createComponent', '⌥⌘K'),
|
||||
...(canCreateComponentSet.value ? [commandMenuItem('selection.createComponentSet', '⇧⌘K')] : []),
|
||||
...(isComponent.value && selectedNode.value
|
||||
? [
|
||||
{
|
||||
label: 'Create instance',
|
||||
action: () => commandMenuItem('selection.createInstance').action?.(),
|
||||
disabled: !commandMenuItem('selection.createInstance').disabled
|
||||
} satisfies MenuActionNode
|
||||
]
|
||||
: []),
|
||||
...(isInstance.value ? [commandMenuItem('selection.goToMainComponent')] : []),
|
||||
...(isInstance.value ? [commandMenuItem('selection.detachInstance', '⌥⌘B')] : []),
|
||||
...(hasSelection.value
|
||||
? [
|
||||
{ separator: true } as MenuSeparatorNode,
|
||||
commandMenuItem('selection.toggleVisibility', '⇧⌘H'),
|
||||
commandMenuItem('selection.toggleLock', '⇧⌘L')
|
||||
]
|
||||
: [])
|
||||
]
|
||||
})
|
||||
|
||||
const selectionLabelMenu = computed(() => ({
|
||||
visibility: editor.getSelectedNode()?.visible ?? true ? 'Hide' : 'Show',
|
||||
lock: editor.getSelectedNode()?.locked ?? false ? 'Unlock' : 'Lock'
|
||||
}))
|
||||
|
||||
return {
|
||||
appMenu,
|
||||
canvasMenu,
|
||||
selectionLabelMenu
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,12 @@ export { useNodeProps, MIXED } from './shared/useNodeProps'
|
|||
export type { MixedValue } from './shared/useNodeProps'
|
||||
export { useSceneComputed } from './shared/useSceneComputed'
|
||||
export { useSelectionState } from './shared/useSelectionState'
|
||||
export { useSelectionCapabilities } from './selection/useSelectionCapabilities'
|
||||
export { useEditorCommands } from './commands/useEditorCommands'
|
||||
export type { EditorCommand, EditorCommandId } from './commands/useEditorCommands'
|
||||
export { useMenuModel } from './commands/useMenuModel'
|
||||
export type { MenuEntry } from './commands/useMenuModel'
|
||||
export { useViewportKind } from './viewport/useViewportKind'
|
||||
export { useLayerDrag } from './shared/useLayerDrag'
|
||||
export { useInlineRename } from './shared/useInlineRename'
|
||||
export { useNodeFontStatus } from './shared/useFontStatus'
|
||||
|
|
|
|||
67
packages/vue/src/selection/useSelectionCapabilities.ts
Normal file
67
packages/vue/src/selection/useSelectionCapabilities.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { computed } from 'vue'
|
||||
|
||||
import { useSelectionState } from '../shared/useSelectionState'
|
||||
|
||||
export function useSelectionCapabilities() {
|
||||
const {
|
||||
editor,
|
||||
selectedIds,
|
||||
selectedNode,
|
||||
selectedCount,
|
||||
hasSelection,
|
||||
isInstance,
|
||||
isGroup,
|
||||
canCreateComponentSet
|
||||
} = useSelectionState()
|
||||
|
||||
const canCopy = computed(() => hasSelection.value)
|
||||
const canCut = computed(() => hasSelection.value)
|
||||
const canDelete = computed(() => hasSelection.value)
|
||||
const canDuplicate = computed(() => hasSelection.value)
|
||||
const canExportSelection = computed(() => hasSelection.value)
|
||||
const canGroup = computed(() => selectedCount.value >= 2)
|
||||
const canUngroup = computed(() => isGroup.value)
|
||||
const canCreateComponent = computed(() => hasSelection.value)
|
||||
const canDetachInstance = computed(() => isInstance.value)
|
||||
const canWrapInAutoLayout = computed(() => hasSelection.value)
|
||||
const canBringToFront = computed(() => hasSelection.value)
|
||||
const canSendToBack = computed(() => hasSelection.value)
|
||||
const canToggleVisibility = computed(() => hasSelection.value)
|
||||
const canToggleLock = computed(() => hasSelection.value)
|
||||
const canGoToMainComponent = computed(() => isInstance.value)
|
||||
const canCreateInstance = computed(() => selectedNode.value?.type === 'COMPONENT')
|
||||
const canMoveToPage = computed(() => hasSelection.value && editor.graph.getPages().length > 1)
|
||||
const canPaste = computed(() => true)
|
||||
const canSelectAll = computed(() => editor.graph.getChildren(editor.state.currentPageId).length > 0)
|
||||
const canUndo = computed(() => editor.undo.canUndo)
|
||||
const canRedo = computed(() => editor.undo.canRedo)
|
||||
const canZoomToSelection = computed(() => hasSelection.value)
|
||||
|
||||
return {
|
||||
selectedIds,
|
||||
selectedNode,
|
||||
canCopy,
|
||||
canCut,
|
||||
canPaste,
|
||||
canDelete,
|
||||
canDuplicate,
|
||||
canExportSelection,
|
||||
canGroup,
|
||||
canUngroup,
|
||||
canCreateComponent,
|
||||
canCreateComponentSet,
|
||||
canDetachInstance,
|
||||
canWrapInAutoLayout,
|
||||
canBringToFront,
|
||||
canSendToBack,
|
||||
canToggleVisibility,
|
||||
canToggleLock,
|
||||
canGoToMainComponent,
|
||||
canCreateInstance,
|
||||
canMoveToPage,
|
||||
canSelectAll,
|
||||
canUndo,
|
||||
canRedo,
|
||||
canZoomToSelection
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { useBreakpoints, useRafFn, useResizeObserver } from '@vueuse/core'
|
||||
import { useRafFn, useResizeObserver } from '@vueuse/core'
|
||||
import { onMounted, onScopeDispose, type Ref } from 'vue'
|
||||
|
||||
import { useViewportKind } from '../viewport/useViewportKind'
|
||||
|
||||
import { getCanvasKit, getGpuBackend, SkiaRenderer } from '@open-pencil/core'
|
||||
|
||||
import type { Editor } from '@open-pencil/core/editor'
|
||||
|
|
@ -133,8 +135,7 @@ export function useCanvas(
|
|||
|
||||
const params = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : new URLSearchParams()
|
||||
const noRulersParam = params.has('no-rulers')
|
||||
const breakpoints = useBreakpoints({ mobile: 768 })
|
||||
const isMobile = breakpoints.smaller('mobile')
|
||||
const { isMobile } = useViewportKind()
|
||||
|
||||
function shouldShowRulers() {
|
||||
if (options?.showRulers === false) return false
|
||||
|
|
|
|||
14
packages/vue/src/viewport/useViewportKind.ts
Normal file
14
packages/vue/src/viewport/useViewportKind.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { useBreakpoints } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const breakpoints = useBreakpoints({ mobile: 768 })
|
||||
|
||||
export function useViewportKind() {
|
||||
const isMobile = breakpoints.smaller('mobile')
|
||||
const isDesktop = computed(() => !isMobile.value)
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
isDesktop
|
||||
}
|
||||
}
|
||||
|
|
@ -18,13 +18,15 @@ import IconChevronRight from '~icons/lucide/chevron-right'
|
|||
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useInlineRename } from '@open-pencil/vue'
|
||||
import { useEditorCommands, useInlineRename, useMenuModel } from '@open-pencil/vue'
|
||||
import { menuContent, menuItem, menuSeparator } from '@/components/ui/menu'
|
||||
import { IS_TAURI } from '@/constants'
|
||||
import { openFileDialog } from '@/composables/use-menu'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
|
||||
const store = useEditorStore()
|
||||
const { menuItem: commandMenuItem } = useEditorCommands()
|
||||
const { appMenu } = useMenuModel()
|
||||
|
||||
const DOCUMENT_NAME_ID = 'document-name'
|
||||
const rename = useInlineRename<'document-name'>((_id, name) => {
|
||||
|
|
@ -95,88 +97,100 @@ const fileMenu: MenuEntry[] = [
|
|||
}
|
||||
]
|
||||
|
||||
const editMenu: MenuEntry[] = [
|
||||
{ label: 'Undo', shortcut: `${mod}Z`, action: () => store.undoAction() },
|
||||
{ label: 'Redo', shortcut: `${mod}⇧Z`, action: () => store.redoAction() },
|
||||
{ separator: true },
|
||||
{ label: 'Copy', shortcut: `${mod}C` },
|
||||
{ label: 'Paste', shortcut: `${mod}V` },
|
||||
{ label: 'Duplicate', shortcut: `${mod}D`, action: () => store.duplicateSelected() },
|
||||
{ label: 'Delete', shortcut: '⌫', action: () => store.deleteSelected() },
|
||||
{ separator: true },
|
||||
{ label: 'Select all', shortcut: `${mod}A`, action: () => store.selectAll() }
|
||||
]
|
||||
|
||||
const viewMenu: MenuEntry[] = [
|
||||
{ label: 'Zoom to 100%', shortcut: `${mod}0`, action: () => store.zoomTo100() },
|
||||
{ label: 'Zoom to fit', shortcut: `${mod}1`, action: () => store.zoomToFit() },
|
||||
{ label: 'Zoom to selection', shortcut: `${mod}2`, action: () => store.zoomToSelection() },
|
||||
{
|
||||
label: 'Zoom in',
|
||||
shortcut: `${mod}=`,
|
||||
action: () => store.applyZoom(-100, window.innerWidth / 2, window.innerHeight / 2)
|
||||
},
|
||||
{
|
||||
label: 'Zoom out',
|
||||
shortcut: `${mod}-`,
|
||||
action: () => store.applyZoom(100, window.innerWidth / 2, window.innerHeight / 2)
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Performance profiler',
|
||||
get checked() {
|
||||
return store.renderer?.profiler.hudVisible ?? false
|
||||
},
|
||||
onCheckedChange: () => {
|
||||
store.toggleProfiler()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const objectMenu: MenuEntry[] = [
|
||||
{ label: 'Group', shortcut: `${mod}G`, action: () => store.groupSelected() },
|
||||
{ label: 'Ungroup', shortcut: `${mod}⇧G`, action: () => store.ungroupSelected() },
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Create component',
|
||||
shortcut: `${mod}⌥K`,
|
||||
action: () => store.createComponentFromSelection()
|
||||
},
|
||||
{
|
||||
label: 'Create component set',
|
||||
action: () => store.createComponentSetFromComponents()
|
||||
},
|
||||
{ label: 'Detach instance', action: () => store.detachInstance() },
|
||||
{ separator: true },
|
||||
{ label: 'Bring to front', shortcut: ']', action: () => store.bringToFront() },
|
||||
{ label: 'Send to back', shortcut: '[', action: () => store.sendToBack() }
|
||||
]
|
||||
|
||||
const textMenu: MenuEntry[] = [
|
||||
{ label: 'Bold', shortcut: `${mod}B` },
|
||||
{ label: 'Italic', shortcut: `${mod}I` },
|
||||
{ label: 'Underline', shortcut: `${mod}U` }
|
||||
]
|
||||
|
||||
const arrangeMenu: MenuEntry[] = [
|
||||
{ label: 'Add auto layout', shortcut: '⇧A', action: () => store.wrapInAutoLayout() },
|
||||
{ separator: true },
|
||||
{ label: 'Align left', shortcut: '⌥A' },
|
||||
{ label: 'Align center', shortcut: '⌥H' },
|
||||
{ label: 'Align right', shortcut: '⌥D' },
|
||||
{ separator: true },
|
||||
{ label: 'Align top', shortcut: '⌥W' },
|
||||
{ label: 'Align middle', shortcut: '⌥V' },
|
||||
{ label: 'Align bottom', shortcut: '⌥S' }
|
||||
]
|
||||
|
||||
const topMenus = [
|
||||
{ label: 'File', items: fileMenu },
|
||||
{ label: 'Edit', items: editMenu },
|
||||
{ label: 'View', items: viewMenu },
|
||||
{ label: 'Object', items: objectMenu },
|
||||
{ label: 'Text', items: textMenu },
|
||||
{ label: 'Arrange', items: arrangeMenu }
|
||||
...appMenu.value.map((menu) => {
|
||||
if (menu.label === 'View') {
|
||||
return {
|
||||
label: 'View',
|
||||
items: [
|
||||
commandMenuItem('view.zoom100', `${mod}0`),
|
||||
commandMenuItem('view.zoomFit', `${mod}1`),
|
||||
commandMenuItem('view.zoomSelection', `${mod}2`),
|
||||
{
|
||||
label: 'Zoom in',
|
||||
shortcut: `${mod}=`,
|
||||
action: () => store.applyZoom(-100, window.innerWidth / 2, window.innerHeight / 2)
|
||||
},
|
||||
{
|
||||
label: 'Zoom out',
|
||||
shortcut: `${mod}-`,
|
||||
action: () => store.applyZoom(100, window.innerWidth / 2, window.innerHeight / 2)
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Performance profiler',
|
||||
get checked() {
|
||||
return store.renderer?.profiler.hudVisible ?? false
|
||||
},
|
||||
onCheckedChange: () => {
|
||||
store.toggleProfiler()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.label === 'Edit') {
|
||||
return {
|
||||
label: 'Edit',
|
||||
items: [
|
||||
commandMenuItem('edit.undo', `${mod}Z`),
|
||||
commandMenuItem('edit.redo', `${mod}⇧Z`),
|
||||
{ separator: true },
|
||||
{ label: 'Copy', shortcut: `${mod}C` },
|
||||
{ label: 'Paste', shortcut: `${mod}V` },
|
||||
commandMenuItem('selection.duplicate', `${mod}D`),
|
||||
commandMenuItem('selection.delete', '⌫'),
|
||||
{ separator: true },
|
||||
commandMenuItem('selection.selectAll', `${mod}A`)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.label === 'Object') {
|
||||
return {
|
||||
label: 'Object',
|
||||
items: [
|
||||
commandMenuItem('selection.group', `${mod}G`),
|
||||
commandMenuItem('selection.ungroup', `${mod}⇧G`),
|
||||
{ separator: true },
|
||||
commandMenuItem('selection.createComponent', `${mod}⌥K`),
|
||||
commandMenuItem('selection.createComponentSet'),
|
||||
commandMenuItem('selection.detachInstance'),
|
||||
{ separator: true },
|
||||
commandMenuItem('selection.bringToFront', ']'),
|
||||
commandMenuItem('selection.sendToBack', '[')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.label === 'Arrange') {
|
||||
return {
|
||||
label: 'Arrange',
|
||||
items: [
|
||||
commandMenuItem('selection.wrapInAutoLayout', '⇧A'),
|
||||
{ separator: true },
|
||||
{ label: 'Align left', shortcut: '⌥A' },
|
||||
{ label: 'Align center', shortcut: '⌥H' },
|
||||
{ label: 'Align right', shortcut: '⌥D' },
|
||||
{ separator: true },
|
||||
{ label: 'Align top', shortcut: '⌥W' },
|
||||
{ label: 'Align middle', shortcut: '⌥V' },
|
||||
{ label: 'Align bottom', shortcut: '⌥S' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return menu
|
||||
}),
|
||||
{ label: 'Text', items: textMenu }
|
||||
]
|
||||
</script>
|
||||
|
||||
|
|
@ -228,24 +242,24 @@ const topMenus = [
|
|||
:class="menuContent({ class: 'min-w-52' })"
|
||||
>
|
||||
<template v-for="(item, i) in menu.items" :key="i">
|
||||
<MenubarSeparator v-if="item.separator" :class="menuSeparator()" />
|
||||
<MenubarSub v-else-if="item.sub">
|
||||
<MenubarSeparator v-if="'separator' in item && item.separator" :class="menuSeparator()" />
|
||||
<MenubarSub v-else-if="'sub' in item && item.sub">
|
||||
<MenubarSubTrigger :class="menuItem()">
|
||||
<span class="flex-1">{{ item.label }}</span>
|
||||
<span class="flex-1">{{ 'label' in item ? item.label : '' }}</span>
|
||||
<IconChevronRight class="size-3 text-muted" />
|
||||
</MenubarSubTrigger>
|
||||
<MenubarPortal>
|
||||
<MenubarSubContent :side-offset="4" :class="menuContent({ class: 'min-w-44' })">
|
||||
<template v-for="(sub, j) in item.sub" :key="j">
|
||||
<MenubarSeparator v-if="sub.separator" :class="menuSeparator()" />
|
||||
<template v-for="(sub, j) in ('sub' in item && item.sub ? item.sub : [])" :key="j">
|
||||
<MenubarSeparator v-if="'separator' in sub && sub.separator" :class="menuSeparator()" />
|
||||
<MenubarItem
|
||||
v-else
|
||||
:class="menuItem()"
|
||||
:disabled="sub.disabled"
|
||||
@select="sub.action?.()"
|
||||
:disabled="'disabled' in sub ? sub.disabled : undefined"
|
||||
@select="'action' in sub ? sub.action?.() : undefined"
|
||||
>
|
||||
<span class="flex-1">{{ sub.label }}</span>
|
||||
<span v-if="sub.shortcut" class="text-[11px] text-muted">{{
|
||||
<span class="flex-1">{{ 'label' in sub ? sub.label : '' }}</span>
|
||||
<span v-if="'shortcut' in sub && sub.shortcut" class="text-[11px] text-muted">{{
|
||||
sub.shortcut
|
||||
}}</span>
|
||||
</MenubarItem>
|
||||
|
|
@ -254,12 +268,12 @@ const topMenus = [
|
|||
</MenubarPortal>
|
||||
</MenubarSub>
|
||||
<MenubarCheckboxItem
|
||||
v-else-if="item.onCheckedChange"
|
||||
:model-value="item.checked"
|
||||
v-else-if="'onCheckedChange' in item && item.onCheckedChange"
|
||||
:model-value="'checked' in item ? item.checked : undefined"
|
||||
:class="menuItem()"
|
||||
@update:model-value="item.onCheckedChange?.($event as boolean)"
|
||||
>
|
||||
<span class="flex-1">{{ item.label }}</span>
|
||||
<span class="flex-1">{{ 'label' in item ? item.label : '' }}</span>
|
||||
<MenubarItemIndicator class="text-surface">
|
||||
<icon-lucide-check class="size-3.5" />
|
||||
</MenubarItemIndicator>
|
||||
|
|
@ -267,11 +281,11 @@ const topMenus = [
|
|||
<MenubarItem
|
||||
v-else
|
||||
:class="menuItem()"
|
||||
:disabled="item.disabled"
|
||||
@select="item.action?.()"
|
||||
:disabled="'disabled' in item ? item.disabled : undefined"
|
||||
@select="'action' in item ? item.action?.() : undefined"
|
||||
>
|
||||
<span class="flex-1">{{ item.label }}</span>
|
||||
<span v-if="item.shortcut" class="text-[11px] text-muted">{{
|
||||
<span class="flex-1">{{ 'label' in item ? item.label : '' }}</span>
|
||||
<span v-if="'shortcut' in item && item.shortcut" class="text-[11px] text-muted">{{
|
||||
item.shortcut
|
||||
}}</span>
|
||||
</MenubarItem>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
|
|
@ -10,7 +9,7 @@ import {
|
|||
ContextMenuPortal
|
||||
} from 'reka-ui'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useSelectionState } from '@open-pencil/vue'
|
||||
import { useEditorCommands, useMenuModel, useSelectionState } from '@open-pencil/vue'
|
||||
import { toast } from '@/utils/toast'
|
||||
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
|
|
@ -19,24 +18,10 @@ import { menuContent, menuItem, menuSeparator } from '@/components/ui/menu'
|
|||
const store = useEditorStore()
|
||||
const { copy } = useClipboard()
|
||||
|
||||
const {
|
||||
editor,
|
||||
selectedIds,
|
||||
hasSelection,
|
||||
selectedCount,
|
||||
selectedNode,
|
||||
isInstance,
|
||||
isComponent,
|
||||
isGroup,
|
||||
canCreateComponentSet
|
||||
} = useSelectionState()
|
||||
const { editor, selectedIds, hasSelection } = useSelectionState()
|
||||
const { getCommand } = useEditorCommands()
|
||||
const { canvasMenu } = useMenuModel()
|
||||
|
||||
const isVisible = computed(() => selectedNode.value?.visible ?? true)
|
||||
const isLocked = computed(() => selectedNode.value?.locked ?? false)
|
||||
|
||||
const otherPages = computed(() =>
|
||||
editor.graph.getPages().filter((p) => p.id !== editor.state.currentPageId)
|
||||
)
|
||||
|
||||
function ids() {
|
||||
return [...selectedIds.value]
|
||||
|
|
@ -84,101 +69,51 @@ const cls = {
|
|||
<ContextMenuItem
|
||||
:class="cls.item"
|
||||
:disabled="!hasSelection"
|
||||
@select="editor.duplicateSelected()"
|
||||
@select="getCommand('selection.duplicate').run()"
|
||||
>
|
||||
<span>Duplicate</span><span class="text-[11px] text-muted">⌘D</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="cls.item" :disabled="!hasSelection" @select="editor.deleteSelected()">
|
||||
<ContextMenuItem :class="cls.item" :disabled="!hasSelection" @select="getCommand('selection.delete').run()">
|
||||
<span>Delete</span><span class="text-[11px] text-muted">⌫</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator :class="cls.sep" />
|
||||
|
||||
<ContextMenuSub v-if="otherPages.length > 0 && hasSelection">
|
||||
<ContextMenuSubTrigger :class="cls.item">
|
||||
<span>Move to page</span><span class="text-sm text-muted">›</span>
|
||||
</ContextMenuSubTrigger>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuSubContent :class="cls.menu">
|
||||
<ContextMenuItem
|
||||
v-for="page in otherPages"
|
||||
:key="page.id"
|
||||
:class="cls.item"
|
||||
@select="editor.moveToPage(page.id)"
|
||||
>
|
||||
{{ page.name }}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuPortal>
|
||||
</ContextMenuSub>
|
||||
|
||||
<ContextMenuItem :class="cls.item" :disabled="!hasSelection" @select="editor.bringToFront()">
|
||||
<span>Bring to front</span><span class="text-[11px] text-muted">]</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="cls.item" :disabled="!hasSelection" @select="editor.sendToBack()">
|
||||
<span>Send to back</span><span class="text-[11px] text-muted">[</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator :class="cls.sep" />
|
||||
|
||||
<ContextMenuItem
|
||||
:class="cls.item"
|
||||
:disabled="selectedCount < 2"
|
||||
@select="editor.groupSelected()"
|
||||
>
|
||||
<span>Group</span><span class="text-[11px] text-muted">⌘G</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="isGroup" :class="cls.item" @select="editor.ungroupSelected()">
|
||||
<span>Ungroup</span><span class="text-[11px] text-muted">⇧⌘G</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="hasSelection" :class="cls.item" @select="editor.wrapInAutoLayout()">
|
||||
<span>Add auto layout</span><span class="text-[11px] text-muted">⇧A</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator :class="cls.sep" />
|
||||
|
||||
<ContextMenuItem
|
||||
:class="cls.component"
|
||||
:disabled="!hasSelection"
|
||||
@select="editor.createComponentFromSelection()"
|
||||
>
|
||||
<span>Create component</span><span class="text-[11px] text-component/60">⌥⌘K</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="canCreateComponentSet"
|
||||
:class="cls.component"
|
||||
@select="editor.createComponentSetFromComponents()"
|
||||
>
|
||||
<span>Create component set</span><span class="text-[11px] text-component/60">⇧⌘K</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="isComponent && selectedNode"
|
||||
:class="cls.component"
|
||||
@select="editor.createInstanceFromComponent(selectedNode.id)"
|
||||
>
|
||||
<span>Create instance</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="isInstance" :class="cls.component" @select="editor.goToMainComponent()">
|
||||
<span>Go to main component</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="isInstance" :class="cls.item" @select="editor.detachInstance()">
|
||||
<span>Detach instance</span><span class="text-[11px] text-muted">⌥⌘B</span>
|
||||
</ContextMenuItem>
|
||||
<template v-for="(item, i) in canvasMenu" :key="`menu-${i}`">
|
||||
<ContextMenuSeparator v-if="item.separator" :class="cls.sep" />
|
||||
<ContextMenuSub v-else-if="item.sub">
|
||||
<ContextMenuSubTrigger :class="cls.item">
|
||||
<span>{{ item.label }}</span><span class="text-sm text-muted">›</span>
|
||||
</ContextMenuSubTrigger>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuSubContent :class="cls.menu">
|
||||
<ContextMenuItem
|
||||
v-for="(sub, j) in item.sub"
|
||||
:key="j"
|
||||
:class="cls.item"
|
||||
:disabled="sub.separator ? true : sub.disabled"
|
||||
@select="!sub.separator && sub.action?.()"
|
||||
>
|
||||
<template v-if="!sub.separator">
|
||||
<span class="flex-1">{{ sub.label }}</span>
|
||||
<span v-if="sub.shortcut" class="text-[11px] text-muted">{{ sub.shortcut }}</span>
|
||||
</template>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuPortal>
|
||||
</ContextMenuSub>
|
||||
<ContextMenuItem
|
||||
v-else
|
||||
:class="item.label.includes('component') || item.label.includes('instance') ? cls.component : cls.item"
|
||||
:disabled="item.disabled"
|
||||
@select="item.action?.()"
|
||||
>
|
||||
<span class="flex-1">{{ item.label }}</span>
|
||||
<span v-if="item.shortcut" class="text-[11px]" :class="item.label.includes('component') || item.label.includes('instance') ? 'text-component/60' : 'text-muted'">{{ item.shortcut }}</span>
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
|
||||
<template v-if="hasSelection">
|
||||
<ContextMenuSeparator :class="cls.sep" />
|
||||
|
||||
<ContextMenuItem :class="cls.item" @select="editor.toggleVisibility()">
|
||||
<span>{{ isVisible ? 'Hide' : 'Show' }}</span
|
||||
><span class="text-[11px] text-muted">⇧⌘H</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="cls.item" @select="editor.toggleLock()">
|
||||
<span>{{ isLocked ? 'Unlock' : 'Lock' }}</span
|
||||
><span class="text-[11px] text-muted">⇧⌘L</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator :class="cls.sep" />
|
||||
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger :class="cls.item">
|
||||
<span>Copy/Paste as</span><span class="text-sm text-muted">›</span>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useEditor } from '@open-pencil/vue'
|
||||
import { useSelectionState, useEditorCommands } from '@open-pencil/vue'
|
||||
|
||||
import VariablesDialog from './VariablesDialog.vue'
|
||||
import AppearanceSection from './properties/AppearanceSection.vue'
|
||||
|
|
@ -15,11 +15,11 @@ import StrokeSection from './properties/StrokeSection.vue'
|
|||
import TypographySection from './properties/TypographySection.vue'
|
||||
import VariablesSection from './properties/VariablesSection.vue'
|
||||
|
||||
const store = useEditor()
|
||||
const variablesOpen = ref(false)
|
||||
|
||||
const node = computed(() => store.getSelectedNode())
|
||||
const multiCount = computed(() => store.getSelectedNodes().length)
|
||||
const { selectedNode: node, selectedCount: multiCount } = useSelectionState()
|
||||
const { getCommand } = useEditorCommands()
|
||||
const goToMainComponent = getCommand('selection.goToMainComponent')
|
||||
const detachInstance = getCommand('selection.detachInstance')
|
||||
const isComponentType = computed(() => {
|
||||
const t = node.value?.type
|
||||
return t === 'COMPONENT' || t === 'COMPONENT_SET' || t === 'INSTANCE'
|
||||
|
|
@ -71,14 +71,14 @@ const isComponentType = computed(() => {
|
|||
<button
|
||||
data-test-id="design-go-to-component"
|
||||
class="rounded bg-component/10 px-2 py-1 text-left text-[11px] text-component hover:bg-component/20"
|
||||
@click="store.goToMainComponent()"
|
||||
@click="goToMainComponent.run()"
|
||||
>
|
||||
Go to Main Component
|
||||
</button>
|
||||
<button
|
||||
data-test-id="design-detach-instance"
|
||||
class="rounded px-2 py-1 text-left text-[11px] text-muted hover:bg-hover"
|
||||
@click="store.detachInstance()"
|
||||
@click="detachInstance.run()"
|
||||
>
|
||||
Detach Instance
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { toast } from '@/utils/toast'
|
|||
import { useEditorStore } from '@/stores/editor'
|
||||
import Tip from '@/components/Tip.vue'
|
||||
import { colorToCSS } from '@open-pencil/core'
|
||||
import { useEditorCommands } from '@open-pencil/vue'
|
||||
import { toolIcons } from '@/utils/tools'
|
||||
import { initials } from '@/utils/text'
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ const { copy } = useClipboard()
|
|||
const collabState = computed(() => collab?.state.value ?? DEFAULT_COLLAB_STATE)
|
||||
const collabPeers = computed(() => collab?.remotePeers.value ?? [])
|
||||
const followingPeer = computed(() => collab?.followingPeer.value ?? null)
|
||||
const { getCommand } = useEditorCommands()
|
||||
|
||||
function onShare() {
|
||||
if (!collab) return
|
||||
|
|
@ -76,7 +78,7 @@ const menuItems: MenuAction[] = [
|
|||
label: 'Export…',
|
||||
action: () => store.exportSelection(1, 'PNG')
|
||||
},
|
||||
{ icon: IconZoomIn, label: 'Zoom to fit', action: () => store.zoomToFit() }
|
||||
{ icon: IconZoomIn, label: 'Zoom to fit', action: () => getCommand('view.zoomFit').run() }
|
||||
]
|
||||
|
||||
const onlineCount = computed(() => collabPeers.value.length + 1)
|
||||
|
|
@ -93,7 +95,7 @@ const onlineCount = computed(() => collabPeers.value.length + 1)
|
|||
<Tip label="Undo">
|
||||
<button
|
||||
class="flex size-8 cursor-pointer items-center justify-center rounded-full border border-white/10 bg-panel/70 shadow-md backdrop-blur-xl select-none active:bg-hover"
|
||||
@click="store.undoAction()"
|
||||
@click="getCommand('edit.undo').run()"
|
||||
>
|
||||
<icon-lucide-undo-2 class="size-3.5 text-surface" />
|
||||
</button>
|
||||
|
|
@ -101,7 +103,7 @@ const onlineCount = computed(() => collabPeers.value.length + 1)
|
|||
<Tip label="Redo">
|
||||
<button
|
||||
class="flex size-8 cursor-pointer items-center justify-center rounded-full border border-white/10 bg-panel/70 shadow-md backdrop-blur-xl select-none active:bg-hover"
|
||||
@click="store.redoAction()"
|
||||
@click="getCommand('edit.redo').run()"
|
||||
>
|
||||
<icon-lucide-redo-2 class="size-3.5 text-surface" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
DropdownMenuItem,
|
||||
DropdownMenuPortal
|
||||
} from 'reka-ui'
|
||||
import { useBreakpoints } from '@vueuse/core'
|
||||
import { AnimatePresence, motion } from 'motion-v'
|
||||
|
||||
import IconChevronDown from '~icons/lucide/chevron-down'
|
||||
|
|
@ -29,13 +28,13 @@ import Tip from '@/components/Tip.vue'
|
|||
import { ACTION_TOAST_DURATION } from '@/constants'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { toolIcons } from '@/utils/tools'
|
||||
import { ToolbarRoot, ToolbarItem } from '@open-pencil/vue'
|
||||
import { ToolbarRoot, ToolbarItem, useEditorCommands, useViewportKind } from '@open-pencil/vue'
|
||||
import type { Component } from 'vue'
|
||||
import type { EditorToolDef, Tool } from '@open-pencil/vue'
|
||||
|
||||
const store = useEditorStore()
|
||||
const breakpoints = useBreakpoints({ mobile: 768 })
|
||||
const isMobile = breakpoints.smaller('mobile')
|
||||
const { isMobile } = useViewportKind()
|
||||
const { getCommand } = useEditorCommands()
|
||||
|
||||
const toolLabels: Record<Tool, string> = {
|
||||
SELECT: 'Move',
|
||||
|
|
@ -85,16 +84,16 @@ const editActions: ActionItem[] = [
|
|||
{ icon: IconCopy, label: 'Copy', action: () => store.mobileCopy() },
|
||||
{ icon: IconClipboard, label: 'Paste', action: () => store.mobilePaste() },
|
||||
{ icon: IconScissors, label: 'Cut', action: () => store.mobileCut() },
|
||||
{ icon: IconCopyPlus, label: 'Duplicate', action: () => store.duplicateSelected() },
|
||||
{ icon: IconTrash2, label: 'Delete', action: () => store.deleteSelected() }
|
||||
{ icon: IconCopyPlus, label: 'Duplicate', action: () => getCommand('selection.duplicate').run() },
|
||||
{ icon: IconTrash2, label: 'Delete', action: () => getCommand('selection.delete').run() }
|
||||
]
|
||||
|
||||
const arrangeActions: ActionItem[] = [
|
||||
{ icon: IconArrowUpToLine, label: 'Front', action: () => store.bringToFront() },
|
||||
{ icon: IconArrowDownToLine, label: 'Back', action: () => store.sendToBack() },
|
||||
{ icon: IconGroup, label: 'Group', action: () => store.groupSelected() },
|
||||
{ icon: IconUngroup, label: 'Ungroup', action: () => store.ungroupSelected() },
|
||||
{ icon: IconLock, label: 'Lock', action: () => store.toggleLock() }
|
||||
{ icon: IconArrowUpToLine, label: 'Front', action: () => getCommand('selection.bringToFront').run() },
|
||||
{ icon: IconArrowDownToLine, label: 'Back', action: () => getCommand('selection.sendToBack').run() },
|
||||
{ icon: IconGroup, label: 'Group', action: () => getCommand('selection.group').run() },
|
||||
{ icon: IconUngroup, label: 'Ungroup', action: () => getCommand('selection.ungroup').run() },
|
||||
{ icon: IconLock, label: 'Lock', action: () => getCommand('selection.toggleLock').run() }
|
||||
]
|
||||
|
||||
const CATEGORY_COUNT = 3
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { useBreakpoints, useEventListener, useMagicKeys, whenever } from '@vueuse/core'
|
||||
import { useEventListener, useMagicKeys, whenever } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useAIChat } from '@/composables/use-chat'
|
||||
import { TOOL_SHORTCUTS, useEditorStore } from '@/stores/editor'
|
||||
import { closeTab, createTab, activeTab as activeTabRef } from '@/stores/tabs'
|
||||
import { extractImageFilesFromClipboard } from '@open-pencil/vue'
|
||||
import { extractImageFilesFromClipboard, useEditorCommands, useViewportKind } from '@open-pencil/vue'
|
||||
|
||||
import { openFileDialog } from './use-menu'
|
||||
|
||||
|
|
@ -55,8 +55,8 @@ function shouldPreventDefault(e: KeyboardEvent, hasPenState: boolean): boolean {
|
|||
export function useKeyboard() {
|
||||
const { activeTab } = useAIChat()
|
||||
const store = useEditorStore()
|
||||
const breakpoints = useBreakpoints({ mobile: 768 })
|
||||
const isMobile = breakpoints.smaller('mobile')
|
||||
const { isMobile } = useViewportKind()
|
||||
const { runCommand } = useEditorCommands()
|
||||
|
||||
useEventListener(window, 'copy', (e: ClipboardEvent) => {
|
||||
if (isEditing(e)) return
|
||||
|
|
@ -121,19 +121,19 @@ export function useKeyboard() {
|
|||
}
|
||||
|
||||
// --- Mod + Alt ---
|
||||
whenever(mod('alt+keyk'), () => store.createComponentFromSelection())
|
||||
whenever(mod('alt+keyb'), () => store.detachInstance())
|
||||
whenever(mod('alt+keyk'), () => runCommand('selection.createComponent'))
|
||||
whenever(mod('alt+keyb'), () => runCommand('selection.detachInstance'))
|
||||
|
||||
// --- Mod + Shift ---
|
||||
whenever(mod('shift+keyk'), () => store.createComponentSetFromComponents())
|
||||
whenever(mod('shift+keyh'), () => store.toggleVisibility())
|
||||
whenever(mod('shift+keyl'), () => store.toggleLock())
|
||||
whenever(mod('shift+keyk'), () => runCommand('selection.createComponentSet'))
|
||||
whenever(mod('shift+keyh'), () => runCommand('selection.toggleVisibility'))
|
||||
whenever(mod('shift+keyl'), () => runCommand('selection.toggleLock'))
|
||||
whenever(mod('shift+keye'), () => {
|
||||
if (store.state.selectedIds.size > 0) void store.exportSelection(1, 'PNG')
|
||||
})
|
||||
whenever(mod('shift+keys'), () => store.saveFigFileAs())
|
||||
whenever(mod('shift+keyg'), () => store.ungroupSelected())
|
||||
whenever(mod('shift+keyz'), () => store.redoAction())
|
||||
whenever(mod('shift+keyg'), () => runCommand('selection.ungroup'))
|
||||
whenever(mod('shift+keyz'), () => runCommand('edit.redo'))
|
||||
|
||||
// --- Mod + Key ---
|
||||
whenever(mod('backslash'), () => {
|
||||
|
|
@ -154,25 +154,25 @@ export function useKeyboard() {
|
|||
})
|
||||
whenever(mod('keyn'), () => createTab())
|
||||
whenever(mod('keyt'), () => createTab())
|
||||
whenever(mod('keyz'), () => store.undoAction())
|
||||
whenever(mod('keyy'), () => store.redoAction())
|
||||
whenever(mod('digit0'), () => store.zoomTo100())
|
||||
whenever(mod('digit1'), () => store.zoomToFit())
|
||||
whenever(mod('digit2'), () => store.zoomToSelection())
|
||||
whenever(mod('keyd'), () => store.duplicateSelected())
|
||||
whenever(mod('keya'), () => store.selectAll())
|
||||
whenever(mod('keyz'), () => runCommand('edit.undo'))
|
||||
whenever(mod('keyy'), () => runCommand('edit.redo'))
|
||||
whenever(mod('digit0'), () => runCommand('view.zoom100'))
|
||||
whenever(mod('digit1'), () => runCommand('view.zoomFit'))
|
||||
whenever(mod('digit2'), () => runCommand('view.zoomSelection'))
|
||||
whenever(mod('keyd'), () => runCommand('selection.duplicate'))
|
||||
whenever(mod('keya'), () => runCommand('selection.selectAll'))
|
||||
whenever(mod('keys'), () => store.saveFigFile())
|
||||
whenever(mod('keyo'), () => openFileDialog())
|
||||
whenever(mod('keyg'), () => store.groupSelected())
|
||||
whenever(mod('keyg'), () => runCommand('selection.group'))
|
||||
|
||||
// --- Shift (no mod) ---
|
||||
whenever(
|
||||
computed(() => keys['shift+digit1'].value && !keys['meta'].value && !keys['control'].value),
|
||||
() => store.zoomToFit()
|
||||
() => runCommand('view.zoomFit')
|
||||
)
|
||||
whenever(
|
||||
computed(() => keys['shift+digit2'].value && !keys['meta'].value && !keys['control'].value),
|
||||
() => store.zoomToSelection()
|
||||
() => runCommand('view.zoomSelection')
|
||||
)
|
||||
whenever(
|
||||
computed(() => keys['shift+keya'].value && !keys['meta'].value && !keys['control'].value),
|
||||
|
|
@ -181,7 +181,7 @@ export function useKeyboard() {
|
|||
if (node?.type === 'FRAME' && store.selectedNodes.value.length === 1) {
|
||||
store.setLayoutMode(node.id, node.layoutMode === 'NONE' ? 'VERTICAL' : 'NONE')
|
||||
} else if (store.selectedNodes.value.length > 0) {
|
||||
store.wrapInAutoLayout()
|
||||
runCommand('selection.wrapInAutoLayout')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -198,10 +198,10 @@ export function useKeyboard() {
|
|||
)
|
||||
}
|
||||
|
||||
whenever(plain('bracketright'), () => store.bringToFront())
|
||||
whenever(plain('bracketleft'), () => store.sendToBack())
|
||||
whenever(plain('backspace'), () => store.deleteSelected())
|
||||
whenever(plain('delete'), () => store.deleteSelected())
|
||||
whenever(plain('bracketright'), () => runCommand('selection.bringToFront'))
|
||||
whenever(plain('bracketleft'), () => runCommand('selection.sendToBack'))
|
||||
whenever(plain('backspace'), () => runCommand('selection.delete'))
|
||||
whenever(plain('delete'), () => runCommand('selection.delete'))
|
||||
whenever(plain('enter'), () => {
|
||||
if (store.state.penState) store.penCommit(false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, provide, ref } from 'vue'
|
||||
import { useBreakpoints, useEventListener, useUrlSearchParams } from '@vueuse/core'
|
||||
import { useEventListener, useUrlSearchParams } from '@vueuse/core'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from 'reka-ui'
|
||||
|
||||
import { provideEditor } from '@open-pencil/vue'
|
||||
import { provideEditor, useViewportKind } from '@open-pencil/vue'
|
||||
import { useKeyboard } from '@/composables/use-keyboard'
|
||||
import { useMenu } from '@/composables/use-menu'
|
||||
import { useCollab, COLLAB_KEY } from '@/composables/use-collab'
|
||||
|
|
@ -33,8 +33,7 @@ const showChrome = !('no-chrome' in params)
|
|||
const firstTab = createTab()
|
||||
const store = useEditorStore()
|
||||
provideEditor(store)
|
||||
const breakpoints = useBreakpoints({ mobile: 768 })
|
||||
const isMobile = breakpoints.smaller('mobile')
|
||||
const { isMobile } = useViewportKind()
|
||||
|
||||
if (route.meta.demo && !('test' in params)) {
|
||||
createDemoShapes(firstTab.store)
|
||||
|
|
|
|||
Loading…
Reference in a new issue