Rename NodeContextMenuContent → CanvasMenu, extract logic
- Rename to CanvasMenu.vue (was NodeContextMenuContent — terrible name) - Move copy-as-text/SVG/JSX to core editor clipboard module - Add useSelectionState() composable to SDK — selection-derived computeds (hasSelection, selectedNode, isInstance, isComponent, etc.) - Eliminate 7x 'void store.state.sceneVersion' reactivity hacks - Component is now mostly declarative template with menu items
This commit is contained in:
parent
492d9dfa75
commit
aa805edf4d
|
|
@ -7,6 +7,8 @@ import {
|
|||
} from '../clipboard'
|
||||
import { collectFontKeys } from '../fonts'
|
||||
import { computeImageHash } from '../figma-api'
|
||||
import { renderNodesToSVG } from '../svg-export'
|
||||
import { selectionToJSX } from '../render'
|
||||
|
||||
import type { Fill, SceneGraph, SceneNode } from '../scene-graph'
|
||||
import type { Vector } from '../types'
|
||||
|
|
@ -279,6 +281,19 @@ export function createClipboardActions(ctx: EditorContext) {
|
|||
}
|
||||
}
|
||||
|
||||
function copySelectionAsText(ids: string[]): string {
|
||||
return ids.map((id) => ctx.graph.getNode(id)?.name ?? id).join('\n')
|
||||
}
|
||||
|
||||
function copySelectionAsSVG(ids: string[]): string | null {
|
||||
const nodeIds = ids.length > 0 ? ids : ctx.graph.getChildren(ctx.state.currentPageId).map((n) => n.id)
|
||||
return renderNodesToSVG(ctx.graph, ctx.state.currentPageId, nodeIds)
|
||||
}
|
||||
|
||||
function copySelectionAsJSX(ids: string[]): string | null {
|
||||
return ids.length > 0 ? selectionToJSX(ids, ctx.graph) : null
|
||||
}
|
||||
|
||||
return {
|
||||
collectSubtrees,
|
||||
centerNodesAt,
|
||||
|
|
@ -289,6 +304,9 @@ export function createClipboardActions(ctx: EditorContext) {
|
|||
warnMissingImages,
|
||||
deleteSelected,
|
||||
storeImage,
|
||||
placeImageFiles
|
||||
placeImageFiles,
|
||||
copySelectionAsText,
|
||||
copySelectionAsSVG,
|
||||
copySelectionAsJSX
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,6 +254,9 @@ export function createEditor(options?: EditorOptions) {
|
|||
storeImage: clipboard.storeImage,
|
||||
placeImageFiles: clipboard.placeImageFiles,
|
||||
loadFontsForNodes: clipboard.loadFontsForNodes,
|
||||
copySelectionAsText: clipboard.copySelectionAsText,
|
||||
copySelectionAsSVG: clipboard.copySelectionAsSVG,
|
||||
copySelectionAsJSX: clipboard.copySelectionAsJSX,
|
||||
|
||||
// Components — bridge functions
|
||||
createComponentFromSelection: () =>
|
||||
|
|
|
|||
50
packages/vue/src/composables/use-selection-state.ts
Normal file
50
packages/vue/src/composables/use-selection-state.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { computed } from 'vue'
|
||||
|
||||
import { useEditor } from '../context'
|
||||
|
||||
import type { SceneNode } from '@open-pencil/core'
|
||||
|
||||
export function useSelectionState() {
|
||||
const editor = useEditor()
|
||||
|
||||
const selectedIds = computed(() => {
|
||||
void editor.state.sceneVersion
|
||||
return editor.state.selectedIds
|
||||
})
|
||||
|
||||
const hasSelection = computed(() => selectedIds.value.size > 0)
|
||||
|
||||
const selectedNode = computed<SceneNode | null>(() => {
|
||||
void editor.state.sceneVersion
|
||||
return editor.getSelectedNode() ?? null
|
||||
})
|
||||
|
||||
const selectedCount = computed(() => selectedIds.value.size)
|
||||
|
||||
const selectedNodeType = computed(() => selectedNode.value?.type ?? null)
|
||||
|
||||
const isInstance = computed(() => selectedNodeType.value === 'INSTANCE')
|
||||
const isComponent = computed(() => selectedNodeType.value === 'COMPONENT')
|
||||
const isGroup = computed(() => selectedNodeType.value === 'GROUP')
|
||||
|
||||
const canCreateComponentSet = computed(() => {
|
||||
if (selectedIds.value.size < 2) return false
|
||||
for (const id of selectedIds.value) {
|
||||
if (editor.graph.getNode(id)?.type !== 'COMPONENT') return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return {
|
||||
editor,
|
||||
selectedIds,
|
||||
hasSelection,
|
||||
selectedNode,
|
||||
selectedCount,
|
||||
selectedNodeType,
|
||||
isInstance,
|
||||
isComponent,
|
||||
isGroup,
|
||||
canCreateComponentSet
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ export { useNodeProps, useNodeProps as useMultiProps, MIXED } from './composable
|
|||
export type { MixedValue } from './composables/use-node-props'
|
||||
export { useInlineRename } from './composables/use-inline-rename'
|
||||
export { useLayerDrag } from './composables/use-layer-drag'
|
||||
export { useSelectionState } from './composables/use-selection-state'
|
||||
export { useNodeFontStatus } from './composables/use-font-status'
|
||||
export { toast } from './toast'
|
||||
export type { Toast, ToastVariant } from './toast'
|
||||
|
|
|
|||
203
src/components/CanvasMenu.vue
Normal file
203
src/components/CanvasMenu.vue
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuPortal
|
||||
} from 'reka-ui'
|
||||
import { useSelectionState, toast } from '@open-pencil/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { menuContent, menuItem, menuSeparator } from '@/components/ui/menu'
|
||||
|
||||
const store = useEditorStore()
|
||||
const {
|
||||
hasSelection,
|
||||
selectedNode,
|
||||
selectedCount,
|
||||
isInstance,
|
||||
isComponent,
|
||||
isGroup,
|
||||
canCreateComponentSet
|
||||
} = useSelectionState()
|
||||
|
||||
const otherPages = computed(() =>
|
||||
store.graph.getPages().filter((p) => p.id !== store.state.currentPageId)
|
||||
)
|
||||
|
||||
const isVisible = computed(() => selectedNode.value?.visible ?? true)
|
||||
const isLocked = computed(() => selectedNode.value?.locked ?? false)
|
||||
|
||||
async function copyAsText() {
|
||||
const text = store.copySelectionAsText([...store.state.selectedIds])
|
||||
await navigator.clipboard.writeText(text)
|
||||
toast.show('Copied as text')
|
||||
}
|
||||
|
||||
async function copyAsSVG() {
|
||||
const svg = store.copySelectionAsSVG([...store.state.selectedIds])
|
||||
if (!svg) return
|
||||
await navigator.clipboard.writeText(svg)
|
||||
toast.show('Copied as SVG')
|
||||
}
|
||||
|
||||
async function copyAsPNG() {
|
||||
const data = await store.renderExportImage([...store.state.selectedIds], 2, 'PNG')
|
||||
if (!data) return
|
||||
const blob = new Blob([data], { type: 'image/png' })
|
||||
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
|
||||
toast.show('Copied as PNG')
|
||||
}
|
||||
|
||||
async function copyAsJSX() {
|
||||
const jsx = store.copySelectionAsJSX([...store.state.selectedIds])
|
||||
if (!jsx) return
|
||||
await navigator.clipboard.writeText(jsx)
|
||||
toast.show('Copied as JSX')
|
||||
}
|
||||
|
||||
function execCommand(cmd: string) {
|
||||
window.document.execCommand(cmd)
|
||||
}
|
||||
|
||||
const cls = {
|
||||
menu: menuContent({
|
||||
class: 'min-w-56 shadow-[0_8px_30px_rgb(0_0_0/0.4)] animate-in fade-in zoom-in-95'
|
||||
}),
|
||||
item: menuItem(),
|
||||
component: menuItem({ tone: 'component' }),
|
||||
sep: menuSeparator({ class: 'my-1' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuContent :class="cls.menu" :side-offset="2" align="start">
|
||||
<ContextMenuItem :class="cls.item" :disabled="!hasSelection" @select="execCommand('copy')">
|
||||
<span>Copy</span><span class="text-[11px] text-muted">⌘C</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="cls.item" :disabled="!hasSelection" @select="execCommand('cut')">
|
||||
<span>Cut</span><span class="text-[11px] text-muted">⌘X</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="cls.item" @select="execCommand('paste')">
|
||||
<span>Paste here</span><span class="text-[11px] text-muted">⌘V</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
:class="cls.item"
|
||||
:disabled="!hasSelection"
|
||||
@select="store.duplicateSelected()"
|
||||
>
|
||||
<span>Duplicate</span><span class="text-[11px] text-muted">⌘D</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="cls.item" :disabled="!hasSelection" @select="store.deleteSelected()">
|
||||
<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="store.moveToPage(page.id)"
|
||||
>
|
||||
{{ page.name }}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuPortal>
|
||||
</ContextMenuSub>
|
||||
|
||||
<ContextMenuItem :class="cls.item" :disabled="!hasSelection" @select="store.bringToFront()">
|
||||
<span>Bring to front</span><span class="text-[11px] text-muted">]</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="cls.item" :disabled="!hasSelection" @select="store.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="store.groupSelected()"
|
||||
>
|
||||
<span>Group</span><span class="text-[11px] text-muted">⌘G</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="isGroup" :class="cls.item" @select="store.ungroupSelected()">
|
||||
<span>Ungroup</span><span class="text-[11px] text-muted">⇧⌘G</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="hasSelection" :class="cls.item" @select="store.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="store.createComponentFromSelection()"
|
||||
>
|
||||
<span>Create component</span><span class="text-[11px] text-component/60">⌥⌘K</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="canCreateComponentSet"
|
||||
:class="cls.component"
|
||||
@select="store.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="store.createInstanceFromComponent(selectedNode.id)"
|
||||
>
|
||||
<span>Create instance</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="isInstance" :class="cls.component" @select="store.goToMainComponent()">
|
||||
<span>Go to main component</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="isInstance" :class="cls.item" @select="store.detachInstance()">
|
||||
<span>Detach instance</span><span class="text-[11px] text-muted">⌥⌘B</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<template v-if="hasSelection">
|
||||
<ContextMenuSeparator :class="cls.sep" />
|
||||
|
||||
<ContextMenuItem :class="cls.item" @select="store.toggleVisibility()">
|
||||
<span>{{ isVisible ? 'Hide' : 'Show' }}</span
|
||||
><span class="text-[11px] text-muted">⇧⌘H</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="cls.item" @select="store.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>
|
||||
</ContextMenuSubTrigger>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuSubContent :class="cls.menu">
|
||||
<ContextMenuItem :class="cls.item" @select="copyAsText">Copy as text</ContextMenuItem>
|
||||
<ContextMenuItem :class="cls.item" @select="copyAsSVG">Copy as SVG</ContextMenuItem>
|
||||
<ContextMenuItem :class="cls.item" @select="copyAsPNG">
|
||||
<span>Copy as PNG</span><span class="text-[11px] text-muted">⇧⌘C</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem :class="cls.item" @select="copyAsJSX">Copy as JSX</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuPortal>
|
||||
</ContextMenuSub>
|
||||
</template>
|
||||
</ContextMenuContent>
|
||||
</template>
|
||||
|
|
@ -5,7 +5,7 @@ import { ContextMenuRoot, ContextMenuTrigger, ContextMenuPortal } from 'reka-ui'
|
|||
import { toolCursor, useCanvas, useCanvasDrop, useCanvasInput, useTextEdit } from '@open-pencil/vue'
|
||||
import { useCollabInjected } from '@/composables/use-collab'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import NodeContextMenuContent from './NodeContextMenuContent.vue'
|
||||
import CanvasMenu from './CanvasMenu.vue'
|
||||
|
||||
const store = useEditorStore()
|
||||
const collab = useCollabInjected()
|
||||
|
|
@ -103,7 +103,7 @@ function onContextMenu(e: MouseEvent) {
|
|||
</ContextMenuTrigger>
|
||||
|
||||
<ContextMenuPortal>
|
||||
<NodeContextMenuContent />
|
||||
<CanvasMenu />
|
||||
</ContextMenuPortal>
|
||||
</ContextMenuRoot>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useInlineRename, useLayerDrag } from '@open-pencil/vue'
|
|||
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import LayerRow from './LayerRow.vue'
|
||||
import NodeContextMenuContent from './NodeContextMenuContent.vue'
|
||||
import CanvasMenu from './CanvasMenu.vue'
|
||||
|
||||
const INDENT_PER_LEVEL = 16
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ function toggleExpand(id: string) {
|
|||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuPortal>
|
||||
<NodeContextMenuContent />
|
||||
<CanvasMenu />
|
||||
</ContextMenuPortal>
|
||||
</ContextMenuRoot>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,337 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuPortal
|
||||
} from 'reka-ui'
|
||||
import { selectionToJSX, renderNodesToSVG } from '@open-pencil/core'
|
||||
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { menuContent, menuItem, menuSeparator } from '@/components/ui/menu'
|
||||
import { toast } from '@open-pencil/vue'
|
||||
|
||||
const store = useEditorStore()
|
||||
|
||||
const hasSelection = computed(() => {
|
||||
void store.state.sceneVersion
|
||||
return store.state.selectedIds.size > 0
|
||||
})
|
||||
|
||||
const singleNode = computed(() => {
|
||||
void store.state.sceneVersion
|
||||
if (store.state.selectedIds.size !== 1) return null
|
||||
const id = [...store.state.selectedIds][0]
|
||||
return store.graph.getNode(id) ?? null
|
||||
})
|
||||
|
||||
const multiCount = computed(() => {
|
||||
void store.state.sceneVersion
|
||||
return store.state.selectedIds.size
|
||||
})
|
||||
|
||||
const isInstance = computed(() => singleNode.value?.type === 'INSTANCE')
|
||||
const isComponent = computed(() => singleNode.value?.type === 'COMPONENT')
|
||||
const isGroup = computed(() => singleNode.value?.type === 'GROUP')
|
||||
|
||||
const canCreateComponentSet = computed(() => {
|
||||
void store.state.sceneVersion
|
||||
if (store.state.selectedIds.size < 2) return false
|
||||
return [...store.state.selectedIds].every((id) => {
|
||||
const n = store.graph.getNode(id)
|
||||
return n?.type === 'COMPONENT'
|
||||
})
|
||||
})
|
||||
|
||||
const otherPages = computed(() => {
|
||||
void store.state.sceneVersion
|
||||
return store.graph.getPages().filter((p) => p.id !== store.state.currentPageId)
|
||||
})
|
||||
|
||||
const isVisible = computed(() => {
|
||||
void store.state.sceneVersion
|
||||
if (!singleNode.value) return true
|
||||
return singleNode.value.visible
|
||||
})
|
||||
|
||||
const isLocked = computed(() => {
|
||||
void store.state.sceneVersion
|
||||
if (!singleNode.value) return false
|
||||
return singleNode.value.locked
|
||||
})
|
||||
|
||||
function selectedIds(): string[] {
|
||||
return [...store.state.selectedIds]
|
||||
}
|
||||
|
||||
async function copyAsText() {
|
||||
const ids = selectedIds()
|
||||
const names = ids.map((id) => store.graph.getNode(id)?.name ?? id).join('\n')
|
||||
await navigator.clipboard.writeText(names)
|
||||
toast.show('Copied as text')
|
||||
}
|
||||
|
||||
async function copyAsSVG() {
|
||||
const ids = selectedIds()
|
||||
const svg = renderNodesToSVG(store.graph, store.state.currentPageId, ids)
|
||||
if (!svg) return
|
||||
await navigator.clipboard.writeText(svg)
|
||||
toast.show('Copied as SVG')
|
||||
}
|
||||
|
||||
async function copyAsPNG() {
|
||||
const ids = selectedIds()
|
||||
const data = await store.renderExportImage(ids, 2, 'PNG')
|
||||
if (!data) return
|
||||
const blob = new Blob([data], { type: 'image/png' })
|
||||
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
|
||||
toast.show('Copied as PNG')
|
||||
}
|
||||
|
||||
function copyAsJSX() {
|
||||
const ids = selectedIds()
|
||||
const jsx = selectionToJSX(ids, store.graph)
|
||||
if (!jsx) return
|
||||
navigator.clipboard.writeText(jsx)
|
||||
toast.show('Copied as JSX')
|
||||
}
|
||||
|
||||
function execCommand(command: string) {
|
||||
window.document.execCommand(command)
|
||||
}
|
||||
|
||||
const itemClass = menuItem()
|
||||
const componentItemClass = menuItem({ tone: 'component' })
|
||||
const menuClass = menuContent({
|
||||
class: 'min-w-56 shadow-[0_8px_30px_rgb(0_0_0/0.4)] animate-in fade-in zoom-in-95'
|
||||
})
|
||||
const separatorClass = menuSeparator({ class: 'my-1' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuContent :class="menuClass" :side-offset="2" align="start">
|
||||
<ContextMenuItem
|
||||
data-test-id="context-copy"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="execCommand('copy')"
|
||||
>
|
||||
<span>Copy</span>
|
||||
<span class="text-[11px] text-muted">⌘C</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-cut"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="execCommand('cut')"
|
||||
>
|
||||
<span>Cut</span>
|
||||
<span class="text-[11px] text-muted">⌘X</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem data-test-id="context-paste" :class="itemClass" @select="execCommand('paste')">
|
||||
<span>Paste here</span>
|
||||
<span class="text-[11px] text-muted">⌘V</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-duplicate"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="store.duplicateSelected()"
|
||||
>
|
||||
<span>Duplicate</span>
|
||||
<span class="text-[11px] text-muted">⌘D</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-delete"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="store.deleteSelected()"
|
||||
>
|
||||
<span>Delete</span>
|
||||
<span class="text-[11px] text-muted">⌫</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator :class="separatorClass" />
|
||||
|
||||
<ContextMenuSub v-if="otherPages.length > 0 && hasSelection">
|
||||
<ContextMenuSubTrigger data-test-id="context-move-to-page" :class="itemClass">
|
||||
<span>Move to page</span>
|
||||
<span class="text-sm text-muted">›</span>
|
||||
</ContextMenuSubTrigger>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuSubContent :class="menuClass">
|
||||
<ContextMenuItem
|
||||
v-for="page in otherPages"
|
||||
:key="page.id"
|
||||
:class="itemClass"
|
||||
@select="store.moveToPage(page.id)"
|
||||
>
|
||||
{{ page.name }}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuPortal>
|
||||
</ContextMenuSub>
|
||||
|
||||
<ContextMenuItem
|
||||
data-test-id="context-bring-to-front"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="store.bringToFront()"
|
||||
>
|
||||
<span>Bring to front</span>
|
||||
<span class="text-[11px] text-muted">]</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-send-to-back"
|
||||
:class="itemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="store.sendToBack()"
|
||||
>
|
||||
<span>Send to back</span>
|
||||
<span class="text-[11px] text-muted">[</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator :class="separatorClass" />
|
||||
|
||||
<ContextMenuItem
|
||||
data-test-id="context-group"
|
||||
:class="itemClass"
|
||||
:disabled="multiCount < 2"
|
||||
@select="store.groupSelected()"
|
||||
>
|
||||
<span>Group</span>
|
||||
<span class="text-[11px] text-muted">⌘G</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="isGroup"
|
||||
data-test-id="context-ungroup"
|
||||
:class="itemClass"
|
||||
@select="store.ungroupSelected()"
|
||||
>
|
||||
<span>Ungroup</span>
|
||||
<span class="text-[11px] text-muted">⇧⌘G</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="hasSelection"
|
||||
data-test-id="context-auto-layout"
|
||||
:class="itemClass"
|
||||
@select="store.wrapInAutoLayout()"
|
||||
>
|
||||
<span>Add auto layout</span>
|
||||
<span class="text-[11px] text-muted">⇧A</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator :class="separatorClass" />
|
||||
|
||||
<ContextMenuItem
|
||||
data-test-id="context-create-component"
|
||||
:class="componentItemClass"
|
||||
:disabled="!hasSelection"
|
||||
@select="store.createComponentFromSelection()"
|
||||
>
|
||||
<span>Create component</span>
|
||||
<span class="text-[11px] text-component/60">⌥⌘K</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="canCreateComponentSet"
|
||||
data-test-id="context-create-component-set"
|
||||
:class="componentItemClass"
|
||||
@select="store.createComponentSetFromComponents()"
|
||||
>
|
||||
<span>Create component set</span>
|
||||
<span class="text-[11px] text-component/60">⇧⌘K</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="isComponent"
|
||||
data-test-id="context-create-instance"
|
||||
:class="componentItemClass"
|
||||
@select="store.createInstanceFromComponent(singleNode!.id)"
|
||||
>
|
||||
<span>Create instance</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="isInstance"
|
||||
data-test-id="context-go-to-component"
|
||||
:class="componentItemClass"
|
||||
@select="store.goToMainComponent()"
|
||||
>
|
||||
<span>Go to main component</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="isInstance"
|
||||
data-test-id="context-detach-instance"
|
||||
:class="itemClass"
|
||||
@select="store.detachInstance()"
|
||||
>
|
||||
<span>Detach instance</span>
|
||||
<span class="text-[11px] text-muted">⌥⌘B</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<template v-if="hasSelection">
|
||||
<ContextMenuSeparator :class="separatorClass" />
|
||||
|
||||
<ContextMenuItem
|
||||
data-test-id="context-toggle-visibility"
|
||||
:class="itemClass"
|
||||
@select="store.toggleVisibility()"
|
||||
>
|
||||
<span>{{ isVisible ? 'Hide' : 'Show' }}</span>
|
||||
<span class="text-[11px] text-muted">⇧⌘H</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-toggle-lock"
|
||||
:class="itemClass"
|
||||
@select="store.toggleLock()"
|
||||
>
|
||||
<span>{{ isLocked ? 'Unlock' : 'Lock' }}</span>
|
||||
<span class="text-[11px] text-muted">⇧⌘L</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator :class="separatorClass" />
|
||||
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger data-test-id="context-copy-paste-as" :class="itemClass">
|
||||
<span>Copy/Paste as</span>
|
||||
<span class="text-sm text-muted">›</span>
|
||||
</ContextMenuSubTrigger>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuSubContent :class="menuClass">
|
||||
<ContextMenuItem
|
||||
data-test-id="context-copy-as-text"
|
||||
:class="itemClass"
|
||||
@select="copyAsText"
|
||||
>
|
||||
Copy as text
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-copy-as-svg"
|
||||
:class="itemClass"
|
||||
@select="copyAsSVG"
|
||||
>
|
||||
Copy as SVG
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-copy-as-png"
|
||||
:class="itemClass"
|
||||
@select="copyAsPNG"
|
||||
>
|
||||
<span>Copy as PNG</span>
|
||||
<span class="text-[11px] text-muted">⇧⌘C</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-copy-as-jsx"
|
||||
:class="itemClass"
|
||||
@select="copyAsJSX"
|
||||
>
|
||||
Copy as JSX
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuPortal>
|
||||
</ContextMenuSub>
|
||||
</template>
|
||||
</ContextMenuContent>
|
||||
</template>
|
||||
Loading…
Reference in a new issue