diff --git a/frontend/src/lib/components/common/contextmenu/ContextMenu.svelte b/frontend/src/lib/components/common/contextmenu/ContextMenu.svelte
index 7302c4948a..b46f1ed7e4 100644
--- a/frontend/src/lib/components/common/contextmenu/ContextMenu.svelte
+++ b/frontend/src/lib/components/common/contextmenu/ContextMenu.svelte
@@ -3,6 +3,14 @@
import { fly } from 'svelte/transition'
import { twMerge } from 'tailwind-merge'
import type { Snippet } from 'svelte'
+ import {
+ getContextMenuContainerClass,
+ CONTEXT_MENU_ITEM_BASE_CLASS,
+ CONTEXT_MENU_ITEM_HOVER_MELT_CLASS,
+ CONTEXT_MENU_ITEM_DISABLED_CLASS,
+ CONTEXT_MENU_DIVIDER_CLASS,
+ CONTEXT_MENU_ANIMATION_CLASSES
+ } from './contextMenuStyles'
export interface ContextMenuItem {
id: string
@@ -61,20 +69,20 @@
{#if $open}
{#each items as menuItem (menuItem.id)}
{#if menuItem.divider}
-
+
{:else}
handleItemClick(menuItem)}
diff --git a/frontend/src/lib/components/common/contextmenu/contextMenuStyles.ts b/frontend/src/lib/components/common/contextmenu/contextMenuStyles.ts
new file mode 100644
index 0000000000..ecbe6c6f87
--- /dev/null
+++ b/frontend/src/lib/components/common/contextmenu/contextMenuStyles.ts
@@ -0,0 +1,45 @@
+/**
+ * Shared styles for context menu components
+ * Ensures visual consistency across all context menu implementations
+ */
+
+/**
+ * Base container styles for context menu
+ * @param zIndex - Optional z-index override (default: 'z-50')
+ */
+export function getContextMenuContainerClass(zIndex: string = 'z-50'): string {
+ return `${zIndex} flex flex-col gap-1 min-w-[12rem] overflow-hidden rounded-md border bg-surface p-1 shadow-md`
+}
+
+/**
+ * Base styles for context menu items
+ */
+export const CONTEXT_MENU_ITEM_BASE_CLASS =
+ 'relative flex cursor-default select-none items-center rounded-md px-2 py-1.5 text-xs outline-none transition-colors'
+
+/**
+ * Hover state styles for context menu items (standard CSS hover)
+ */
+export const CONTEXT_MENU_ITEM_HOVER_CLASS = 'hover:bg-surface-hover'
+
+/**
+ * Hover state styles for context menu items (Melt UI data attribute)
+ */
+export const CONTEXT_MENU_ITEM_HOVER_MELT_CLASS = 'data-[highlighted]:bg-surface-hover'
+
+/**
+ * Disabled state styles for context menu items
+ */
+export const CONTEXT_MENU_ITEM_DISABLED_CLASS = 'pointer-events-none opacity-50'
+
+/**
+ * Divider styles for context menu
+ */
+export const CONTEXT_MENU_DIVIDER_CLASS = 'my-1 h-px bg-border-light'
+
+/**
+ * Melt UI animation classes for context menu
+ */
+export const CONTEXT_MENU_ANIMATION_CLASSES =
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2'
+
diff --git a/frontend/src/lib/components/graph/FlowGraphV2.svelte b/frontend/src/lib/components/graph/FlowGraphV2.svelte
index 316bb7a7a8..700925cc2c 100644
--- a/frontend/src/lib/components/graph/FlowGraphV2.svelte
+++ b/frontend/src/lib/components/graph/FlowGraphV2.svelte
@@ -64,6 +64,7 @@
import SelectionBoundingBox from './SelectionBoundingBox.svelte'
import SelectionTool from './SelectionTool.svelte'
import NodeContextMenu from './NodeContextMenu.svelte'
+ import PaneContextMenu from './PaneContextMenu.svelte'
import { SelectionManager } from './selectionUtils.svelte'
import { ChangeTracker } from '$lib/svelte5Utils.svelte'
import { NoteManager } from './noteManager.svelte'
@@ -235,6 +236,9 @@
// Runtime text height tracking for notes (not stored in FlowNote)
let noteTextHeights = $state
>({})
+ // Reference to pane context menu component
+ let paneContextMenu: PaneContextMenu | undefined = $state(undefined)
+
// Selection manager - create one if not provided
let selectionManager = selectionManagerProp || new SelectionManager()
const selectedId = $derived(selectionManager.getSelectedId())
@@ -753,6 +757,7 @@
/>
{/if}
+
{
document.dispatchEvent(new Event('focus'))
@@ -761,6 +766,9 @@
}
noteManager.clearNoteSelection()
}}
+ onpanecontextmenu={({ event }) => {
+ paneContextMenu?.onPaneContextMenu(event)
+ }}
onnodedragstop={(event) => {
const node = event.targetNode
if (node && node.type === 'note') {
diff --git a/frontend/src/lib/components/graph/PaneContextMenu.svelte b/frontend/src/lib/components/graph/PaneContextMenu.svelte
new file mode 100644
index 0000000000..2bf0765134
--- /dev/null
+++ b/frontend/src/lib/components/graph/PaneContextMenu.svelte
@@ -0,0 +1,109 @@
+
+
+{#if contextMenuVisible}
+
+
+
+
+ {
+ contextMenuVisible = false
+ }}
+ oncontextmenu={(e) => {
+ e.preventDefault()
+ contextMenuVisible = false
+ }}
+ >
+{/if}