This commit is contained in:
wkh
2026-08-29 17:47:12 +02:00
commit c7885067e7
53 changed files with 13458 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
<script setup lang="ts">
import { onBeforeUnmount, watch } from 'vue';
interface Item {
label: string;
action: () => void;
}
const props = defineProps<{ open: boolean; x: number; y: number; items: Item[] }>();
const emit = defineEmits<{ (e: 'update:open', v: boolean): void }>();
function close() {
emit('update:open', false);
}
function onDocClick() {
close();
}
function onKey(e: KeyboardEvent) {
if (e.key === 'Escape') close();
}
watch(
() => props.open,
(open) => {
if (open) {
// defer so the opening contextmenu event doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', onDocClick);
document.addEventListener('keydown', onKey);
});
} else {
document.removeEventListener('click', onDocClick);
document.removeEventListener('keydown', onKey);
}
},
);
onBeforeUnmount(() => {
document.removeEventListener('click', onDocClick);
document.removeEventListener('keydown', onKey);
});
function run(item: Item) {
item.action();
close();
}
</script>
<template>
<Teleport to="body">
<ul
v-if="open"
class="ctx"
:style="{ left: x + 'px', top: y + 'px' }"
@click.stop
>
<li v-for="item in items" :key="item.label">
<button type="button" @click="run(item)">{{ item.label }}</button>
</li>
</ul>
</Teleport>
</template>
<style scoped>
.ctx {
position: fixed;
z-index: 1000;
min-width: 200px;
margin: 0;
padding: 6px;
list-style: none;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow);
}
.ctx button {
width: 100%;
text-align: left;
border: 0;
background: transparent;
padding: 7px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.ctx button:hover {
background: var(--accent);
color: #fff;
}
</style>