wraith/frontend/src/components/common/ContextMenu.vue
Vantz Stockwell 8a096d7f7b
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Has been cancelled
Wraith v0.1.0 — Desktop SSH + RDP + SFTP Client
Go + Wails v3 + Vue 3 + SQLite + FreeRDP3 (purego)
183 tests, 76 source files, 9,910 lines of code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 08:19:29 -04:00

98 lines
2.7 KiB
Vue

<template>
<Teleport to="body">
<div
v-if="visible"
class="fixed inset-0 z-50"
@click="close"
@contextmenu.prevent="close"
>
<div
ref="menuRef"
class="fixed bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl py-1 min-w-[160px] overflow-hidden"
:style="{ left: position.x + 'px', top: position.y + 'px' }"
@click.stop
>
<template v-for="(item, idx) in items" :key="idx">
<!-- Separator -->
<div v-if="item.separator" class="my-1 border-t border-[#30363d]" />
<!-- Menu item -->
<button
v-else
class="w-full flex items-center gap-2.5 px-3 py-1.5 text-xs text-left transition-colors cursor-pointer"
:class="item.danger
? 'text-[var(--wraith-accent-red)] hover:bg-[var(--wraith-accent-red)]/10'
: 'text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)]'
"
@click="handleClick(item)"
>
<span v-if="item.icon" class="w-4 h-4 flex items-center justify-center shrink-0" v-html="item.icon" />
<span class="flex-1">{{ item.label }}</span>
<span v-if="item.shortcut" class="text-[10px] text-[var(--wraith-text-muted)]">{{ item.shortcut }}</span>
</button>
</template>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, nextTick } from "vue";
export interface ContextMenuItem {
label?: string;
icon?: string;
shortcut?: string;
danger?: boolean;
separator?: boolean;
action?: () => void;
}
const visible = ref(false);
const position = ref({ x: 0, y: 0 });
const items = ref<ContextMenuItem[]>([]);
const menuRef = ref<HTMLDivElement | null>(null);
function open(event: MouseEvent, menuItems: ContextMenuItem[]): void {
items.value = menuItems;
visible.value = true;
// Position at cursor, adjusting if near viewport edges
nextTick(() => {
const menu = menuRef.value;
if (!menu) {
position.value = { x: event.clientX, y: event.clientY };
return;
}
let x = event.clientX;
let y = event.clientY;
const menuWidth = menu.offsetWidth;
const menuHeight = menu.offsetHeight;
if (x + menuWidth > window.innerWidth) {
x = window.innerWidth - menuWidth - 4;
}
if (y + menuHeight > window.innerHeight) {
y = window.innerHeight - menuHeight - 4;
}
position.value = { x, y };
});
}
function close(): void {
visible.value = false;
}
function handleClick(item: ContextMenuItem): void {
if (item.action) {
item.action();
}
close();
}
defineExpose({ open, close, visible });
</script>