Add viewport controls, collapsible panels with persisted state, filters modal, and refined context menus; preserve map layout on navigation and let contacts be edited with relations on /contacts/:id instead of list modals. Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
2.1 KiB
Vue
105 lines
2.1 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="open"
|
|
class="graph-node-menu-overlay"
|
|
@click="close"
|
|
@contextmenu.prevent="close"
|
|
/>
|
|
<div
|
|
v-if="open"
|
|
class="graph-node-menu"
|
|
:style="{ left: `${x}px`, top: `${y}px` }"
|
|
role="menu"
|
|
@click.stop
|
|
@contextmenu.prevent
|
|
>
|
|
<button
|
|
v-for="item in actions"
|
|
:key="item.id"
|
|
type="button"
|
|
class="graph-node-menu__item"
|
|
role="menuitem"
|
|
@click="onSelect(item.id)"
|
|
>
|
|
{{ item.label }}
|
|
</button>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, onUnmounted, watch } from 'vue'
|
|
|
|
const props = defineProps({
|
|
open: { type: Boolean, default: false },
|
|
x: { type: Number, default: 0 },
|
|
y: { type: Number, default: 0 },
|
|
actions: {
|
|
type: Array,
|
|
default: () => [{ id: 'create-contact', label: 'Добавить контакт' }],
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits(['close', 'create-contact', 'select'])
|
|
|
|
function close() {
|
|
emit('close')
|
|
}
|
|
|
|
function onSelect(id) {
|
|
emit('select', id)
|
|
if (id === 'create-contact') emit('create-contact')
|
|
close()
|
|
}
|
|
|
|
function onKeyDown(event) {
|
|
if (event.key === 'Escape' && props.open) close()
|
|
}
|
|
|
|
watch(() => props.open, (isOpen) => {
|
|
if (isOpen) window.addEventListener('keydown', onKeyDown)
|
|
else window.removeEventListener('keydown', onKeyDown)
|
|
})
|
|
|
|
onMounted(() => {
|
|
if (props.open) window.addEventListener('keydown', onKeyDown)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('keydown', onKeyDown)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.graph-node-menu-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 900;
|
|
}
|
|
.graph-node-menu {
|
|
position: fixed;
|
|
z-index: 901;
|
|
min-width: 180px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: var(--shadow);
|
|
padding: 6px 0;
|
|
}
|
|
.graph-node-menu__item {
|
|
display: block;
|
|
width: 100%;
|
|
text-align: left;
|
|
padding: 8px 14px;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
}
|
|
.graph-node-menu__item:hover {
|
|
background: var(--accent-dim);
|
|
}
|
|
</style>
|