From a2b01f787ed8bd4a213cbba220e31b26137ebe66 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 12 Oct 2026 14:54:32 +0330 Subject: [PATCH] club home redesign; deal card menu anchored to button + delete --- apps/web/src/components/pipeline/DealCard.vue | 1 + apps/web/src/lib/anchored-menu.ts | 43 +++++++++++++++++++ apps/web/src/views/PipelineBoardView.vue | 17 +++++++- apps/web/src/views/ProjectBoardView.vue | 12 +++++- 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/lib/anchored-menu.ts diff --git a/apps/web/src/components/pipeline/DealCard.vue b/apps/web/src/components/pipeline/DealCard.vue index 802154b..8374be3 100644 --- a/apps/web/src/components/pipeline/DealCard.vue +++ b/apps/web/src/components/pipeline/DealCard.vue @@ -48,6 +48,7 @@ const extraAssignees = computed(() =>
| null { + const btn = document.querySelector( + `[data-card-id="${CSS.escape(cardId)}"] .menu`, + ); + if (!btn) return null; + const r = btn.getBoundingClientRect(); + const gap = 4; + const left = Math.min(Math.max(8, r.left), window.innerWidth - width - 8); + const fitsBelow = r.bottom + gap + height <= window.innerHeight; + const top = fitsBelow ? r.bottom + gap : Math.max(8, r.top - height - gap); + return { inset: 'auto', top: `${top}px`, left: `${left}px`, minWidth: `${width}px` }; +} + +/** بستن منو با کلیک بیرون، Escape یا اسکرول */ +export function bindMenuDismiss(isOpen: () => boolean, close: () => void): () => void { + const onPointer = (e: PointerEvent) => { + if (!isOpen()) return; + const t = e.target as Element | null; + if (t?.closest('.menu-pop, .deal-card .menu')) return; + close(); + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen()) close(); + }; + const onScroll = (e: Event) => { + if (!isOpen()) return; + if ((e.target as Element | null)?.closest?.('.menu-pop')) return; + close(); + }; + window.addEventListener('pointerdown', onPointer, true); + window.addEventListener('keydown', onKey); + window.addEventListener('scroll', onScroll, true); + return () => { + window.removeEventListener('pointerdown', onPointer, true); + window.removeEventListener('keydown', onKey); + window.removeEventListener('scroll', onScroll, true); + }; +} diff --git a/apps/web/src/views/PipelineBoardView.vue b/apps/web/src/views/PipelineBoardView.vue index 1b76ac7..c62de03 100644 --- a/apps/web/src/views/PipelineBoardView.vue +++ b/apps/web/src/views/PipelineBoardView.vue @@ -42,6 +42,7 @@ const dealClientReadOptions = { }; import { useAuthStore } from '@/stores/auth'; import { canManageSettings } from '@/lib/roles'; +import { bindMenuDismiss, cardMenuStyle } from '@/lib/anchored-menu'; const route = useRoute(); const router = useRouter(); @@ -507,10 +508,18 @@ function onImportSettled() { pageBusy.value = false; } +const menuStyle = ref | null>(null); + function onMenuDeal(dealId: string) { menuOpenId.value = menuOpenId.value === dealId ? null : dealId; + menuStyle.value = menuOpenId.value ? cardMenuStyle(dealId) : null; } +const unbindMenuDismiss = bindMenuDismiss( + () => menuOpenId.value != null, + () => (menuOpenId.value = null), +); + watch( () => route.query.pipelineId, (id, prev) => { @@ -526,6 +535,7 @@ onMounted(() => { }); onBeforeUnmount(() => { + unbindMenuDismiss(); window.removeEventListener('drag', onPointerMove); window.removeEventListener('dragover', onPointerMove); }); @@ -619,7 +629,7 @@ defineExpose({ updateDealStage: applyUpdateDealStage }); /> -