From 004d1dff93cea866ad98b7d36ca1738511fefd83 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Wed, 4 Nov 2026 20:05:50 +0330 Subject: [PATCH] feat(boards): ease dragging a card toward an off-screen spot Scroll the column while a card is held near its edge and show a light gap where the card will land. Co-authored-by: Cursor --- apps/web/src/components/pipeline/DealCard.vue | 6 +- .../components/pipeline/PipelineColumn.vue | 61 +++++++++++++--- apps/web/src/lib/pipeline-board.ts | 37 ++++++++++ apps/web/src/views/LeadBoardView.vue | 72 +++++++++++++++---- 4 files changed, 153 insertions(+), 23 deletions(-) diff --git a/apps/web/src/components/pipeline/DealCard.vue b/apps/web/src/components/pipeline/DealCard.vue index c792160..0e5733b 100644 --- a/apps/web/src/components/pipeline/DealCard.vue +++ b/apps/web/src/components/pipeline/DealCard.vue @@ -76,11 +76,13 @@ const priorityFlag = computed(() => const cardStyle = computed(() => ({ cursor: props.selectMode ? 'pointer' : 'grab', - opacity: props.dragging ? 0.45 : 1, + opacity: props.dragging ? 0.4 : 1, + transform: props.dragging ? 'scale(0.98)' : 'none', + transition: 'opacity 180ms ease, transform 180ms ease, box-shadow 180ms ease', margin: 0, background: props.selected ? token.value.colorPrimaryBg : token.value.colorBgContainer, borderColor: props.selected ? token.value.colorPrimary : token.value.colorBorderSecondary, - boxShadow: token.value.boxShadowTertiary, + boxShadow: props.dragging ? token.value.boxShadow : token.value.boxShadowTertiary, })); diff --git a/apps/web/src/components/pipeline/PipelineColumn.vue b/apps/web/src/components/pipeline/PipelineColumn.vue index d77b035..e71ddac 100644 --- a/apps/web/src/components/pipeline/PipelineColumn.vue +++ b/apps/web/src/components/pipeline/PipelineColumn.vue @@ -13,7 +13,7 @@ import { import AddDealButton from './AddDealButton.vue'; import DealCard from './DealCard.vue'; import type { PipelineDeal, PipelineStage, PipelineUiCopy } from '@/lib/pipeline-board'; -import { DEAL_BOARD_COPY, PIPELINE_ACCENT, pointerDropIndex } from '@/lib/pipeline-board'; +import { DEAL_BOARD_COPY, PIPELINE_ACCENT, dragScrollColumn, pointerDropIndex, stopDragScroll } from '@/lib/pipeline-board'; const props = withDefaults( defineProps<{ @@ -51,6 +51,8 @@ const emit = defineEmits<{ const { Text: ATypographyText } = Typography; const { token } = theme.useToken(); +const bodyEl = ref(null); +const insertAt = ref(null); const renaming = ref(false); const draft = ref(''); const titleInput = ref<{ focus: () => void; select?: () => void } | null>(null); @@ -106,16 +108,49 @@ const bodyStyle = computed(() => ({ margin: 0, })); -function onCardDragOver(stageId: string, event: DragEvent) { +function trackDrag(event: DragEvent, index: number | null) { event.preventDefault(); + dragScrollColumn(bodyEl.value, event.clientY); + if (index != null) insertAt.value = index; +} + +function clearDragUi() { + insertAt.value = null; + stopDragScroll(); +} + +function onCardDragOver(stageId: string, index: number, event: DragEvent) { event.stopPropagation(); + trackDrag(event, pointerDropIndex(index, event)); emit('dragOver', stageId, event); } +function onColumnDrop(event: DragEvent) { + const at = insertAt.value ?? undefined; + clearDragUi(); + emit('drop', props.stage.id, event, at); +} + function onCardDrop(stageId: string, index: number, event: DragEvent) { event.preventDefault(); event.stopPropagation(); - emit('drop', stageId, event, pointerDropIndex(index, event)); + const at = insertAt.value ?? pointerDropIndex(index, event); + clearDragUi(); + emit('drop', stageId, event, at); +} + +function slotStyle(active: boolean, collapseGap = false) { + return { + height: active ? '36px' : '0px', + marginTop: !active && collapseGap ? '-12px' : '0', + borderRadius: `${token.value.borderRadius}px`, + background: active ? token.value.colorPrimaryBg : 'transparent', + outline: active ? `1px dashed ${token.value.colorPrimary}` : 'none', + outlineOffset: '-1px', + transition: 'height 180ms ease, margin-top 180ms ease, background 180ms ease', + overflow: 'hidden', + flexShrink: 0, + }; } function startRename() { @@ -174,8 +209,8 @@ function onMenuClick({ key }: { key: string | number }) {