From aa805edf4d98e89b19d8068f223413d58de9cc12 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Tue, 17 Mar 2026 08:52:32 +0300 Subject: [PATCH] =?UTF-8?q?Rename=20NodeContextMenuContent=20=E2=86=92=20C?= =?UTF-8?q?anvasMenu,=20extract=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- packages/core/src/editor/clipboard.ts | 20 +- packages/core/src/editor/create.ts | 3 + .../src/composables/use-selection-state.ts | 50 +++ packages/vue/src/index.ts | 1 + src/components/CanvasMenu.vue | 203 +++++++++++ src/components/EditorCanvas.vue | 4 +- src/components/LayerTree.vue | 4 +- src/components/NodeContextMenuContent.vue | 337 ------------------ 8 files changed, 280 insertions(+), 342 deletions(-) create mode 100644 packages/vue/src/composables/use-selection-state.ts create mode 100644 src/components/CanvasMenu.vue delete mode 100644 src/components/NodeContextMenuContent.vue diff --git a/packages/core/src/editor/clipboard.ts b/packages/core/src/editor/clipboard.ts index ce638f25c..9763ba07e 100644 --- a/packages/core/src/editor/clipboard.ts +++ b/packages/core/src/editor/clipboard.ts @@ -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 } } diff --git a/packages/core/src/editor/create.ts b/packages/core/src/editor/create.ts index 5e82baff5..b87b0c82f 100644 --- a/packages/core/src/editor/create.ts +++ b/packages/core/src/editor/create.ts @@ -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: () => diff --git a/packages/vue/src/composables/use-selection-state.ts b/packages/vue/src/composables/use-selection-state.ts new file mode 100644 index 000000000..42f3ef598 --- /dev/null +++ b/packages/vue/src/composables/use-selection-state.ts @@ -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(() => { + 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 + } +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 701b27e01..e08781ab3 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -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' diff --git a/src/components/CanvasMenu.vue b/src/components/CanvasMenu.vue new file mode 100644 index 000000000..3b7b9b584 --- /dev/null +++ b/src/components/CanvasMenu.vue @@ -0,0 +1,203 @@ + + + diff --git a/src/components/EditorCanvas.vue b/src/components/EditorCanvas.vue index 556288b35..4b7a9c59f 100644 --- a/src/components/EditorCanvas.vue +++ b/src/components/EditorCanvas.vue @@ -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) { - + diff --git a/src/components/LayerTree.vue b/src/components/LayerTree.vue index 0b64904de..ca8421d51 100644 --- a/src/components/LayerTree.vue +++ b/src/components/LayerTree.vue @@ -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) { - + diff --git a/src/components/NodeContextMenuContent.vue b/src/components/NodeContextMenuContent.vue deleted file mode 100644 index 9062d92be..000000000 --- a/src/components/NodeContextMenuContent.vue +++ /dev/null @@ -1,337 +0,0 @@ - - -