diff --git a/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte index dfbf0dde2..8b1f4fac6 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte @@ -140,6 +140,7 @@ let resizeOriginalEnd = $state(null); let resizePreviewTop = $state(0); let resizePreviewHeight = $state(0); + let resizeOffsetMinutes = $state(0); // Track if we actually moved during drag/resize (to prevent click on simple mousedown/up) let hasMoved = $state(false); @@ -276,10 +277,19 @@ resizeOriginalEnd = end; const startMinutes = start.getHours() * 60 + start.getMinutes(); + const endMinutes = end.getHours() * 60 + end.getMinutes(); const duration = differenceInMinutes(end, start); resizePreviewTop = minutesToPercent(startMinutes); resizePreviewHeight = (duration / (totalVisibleHours * 60)) * 100; + // Calculate offset between snapped click position and actual event boundary + const clickMinutes = getMinutesFromY(e.clientY); + if (edge === 'top') { + resizeOffsetMinutes = clickMinutes - startMinutes; + } else { + resizeOffsetMinutes = clickMinutes - endMinutes; + } + document.addEventListener('pointermove', handleResizeMove); document.addEventListener('pointerup', handleResizeEnd); } @@ -289,18 +299,19 @@ hasMoved = true; const mouseMinutes = getMinutesFromY(e.clientY); - const snappedMinutes = snapToGrid(mouseMinutes); + // Apply offset to prevent jumping when drag starts + const adjustedMinutes = snapToGrid(mouseMinutes - resizeOffsetMinutes); const origStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const origEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); if (resizeEdge === 'top') { - const newStartMinutes = Math.min(snappedMinutes, origEndMinutes - SNAP_MINUTES); + const newStartMinutes = Math.min(adjustedMinutes, origEndMinutes - SNAP_MINUTES); const clampedStart = Math.max(firstVisibleHour * 60, newStartMinutes); resizePreviewTop = minutesToPercent(clampedStart); resizePreviewHeight = ((origEndMinutes - clampedStart) / (totalVisibleHours * 60)) * 100; } else { - const newEndMinutes = Math.max(snappedMinutes, origStartMinutes + SNAP_MINUTES); + const newEndMinutes = Math.max(adjustedMinutes, origStartMinutes + SNAP_MINUTES); const clampedEnd = Math.min(lastVisibleHour * 60, newEndMinutes); resizePreviewHeight = ((clampedEnd - origStartMinutes) / (totalVisibleHours * 60)) * 100; } @@ -313,7 +324,8 @@ } const mouseMinutes = getMinutesFromY(e.clientY); - const snappedMinutes = snapToGrid(mouseMinutes); + // Apply offset to prevent jumping + const adjustedMinutes = snapToGrid(mouseMinutes - resizeOffsetMinutes); const origStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const origEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); @@ -324,7 +336,7 @@ if (resizeEdge === 'top') { const newStartMinutes = Math.max( firstVisibleHour * 60, - Math.min(snappedMinutes, origEndMinutes - SNAP_MINUTES) + Math.min(adjustedMinutes, origEndMinutes - SNAP_MINUTES) ); newStart = setHours(new Date(viewStore.currentDate), Math.floor(newStartMinutes / 60)); newStart = setMinutes(newStart, newStartMinutes % 60); @@ -332,7 +344,7 @@ } else { const newEndMinutes = Math.min( lastVisibleHour * 60, - Math.max(snappedMinutes, origStartMinutes + SNAP_MINUTES) + Math.max(adjustedMinutes, origStartMinutes + SNAP_MINUTES) ); newEnd = setHours(new Date(viewStore.currentDate), Math.floor(newEndMinutes / 60)); newEnd = setMinutes(newEnd, newEndMinutes % 60); @@ -362,6 +374,7 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; document.removeEventListener('pointermove', handleDragMove); document.removeEventListener('pointerup', handleDragEnd); diff --git a/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte index 1a8910595..fba7ea22a 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte @@ -97,6 +97,7 @@ let resizeOriginalEnd = $state(null); let resizePreviewTop = $state(0); let resizePreviewHeight = $state(0); + let resizeOffsetMinutes = $state(0); // Track if we actually moved during drag/resize (to prevent click on simple mousedown/up) let hasMoved = $state(false); @@ -431,10 +432,19 @@ // Set initial preview const startMinutes = start.getHours() * 60 + start.getMinutes(); + const endMinutes = end.getHours() * 60 + end.getMinutes(); const duration = differenceInMinutes(end, start); resizePreviewTop = minutesToPercent(startMinutes); resizePreviewHeight = (duration / (totalVisibleHours * 60)) * 100; + // Calculate offset between snapped click position and actual event boundary + const clickMinutes = getMinutesFromY(e.clientY); + if (edge === 'top') { + resizeOffsetMinutes = clickMinutes - startMinutes; + } else { + resizeOffsetMinutes = clickMinutes - endMinutes; + } + document.addEventListener('pointermove', handleResizeMove); document.addEventListener('pointerup', handleResizeEnd); } @@ -444,6 +454,8 @@ hasMoved = true; const currentMinutes = getMinutesFromY(e.clientY); + // Apply offset to prevent jumping when drag starts + const adjustedMinutes = currentMinutes - resizeOffsetMinutes; const originalStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); @@ -452,7 +464,7 @@ // Resize from bottom - change end time const newEndMinutes = Math.max( originalStartMinutes + 15, - Math.min(lastVisibleHour * 60, currentMinutes) + Math.min(lastVisibleHour * 60, adjustedMinutes) ); const newDuration = newEndMinutes - originalStartMinutes; resizePreviewHeight = (newDuration / (totalVisibleHours * 60)) * 100; @@ -460,7 +472,7 @@ // Resize from top - change start time const newStartMinutes = Math.max( firstVisibleHour * 60, - Math.min(originalEndMinutes - 15, currentMinutes) + Math.min(originalEndMinutes - 15, adjustedMinutes) ); const newDuration = originalEndMinutes - newStartMinutes; resizePreviewTop = minutesToPercent(newStartMinutes); @@ -477,11 +489,14 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; return; } const currentMinutes = getMinutesFromY(e.clientY); + // Apply offset to prevent jumping + const adjustedMinutes = currentMinutes - resizeOffsetMinutes; const originalStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); @@ -492,7 +507,7 @@ if (resizeEdge === 'bottom') { const newEndMinutes = Math.max( originalStartMinutes + 15, - Math.min(lastVisibleHour * 60, currentMinutes) + Math.min(lastVisibleHour * 60, adjustedMinutes) ); const newHours = Math.floor(newEndMinutes / 60); const newMins = newEndMinutes % 60; @@ -501,7 +516,7 @@ } else { const newStartMinutes = Math.max( firstVisibleHour * 60, - Math.min(originalEndMinutes - 15, currentMinutes) + Math.min(originalEndMinutes - 15, adjustedMinutes) ); const newHours = Math.floor(newStartMinutes / 60); const newMins = newStartMinutes % 60; @@ -527,6 +542,7 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; } @@ -789,6 +805,7 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; isTaskDragging = false; draggedTask = null; taskDragTargetDay = null; diff --git a/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte index 9e2f3c9f5..38045db41 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte @@ -96,6 +96,7 @@ let resizeOriginalEnd = $state(null); let resizePreviewTop = $state(0); let resizePreviewHeight = $state(0); + let resizeOffsetMinutes = $state(0); // Track if we actually moved during drag/resize (to prevent click on simple mousedown/up) let hasMoved = $state(false); @@ -438,10 +439,19 @@ // Set initial preview const startMinutes = start.getHours() * 60 + start.getMinutes(); + const endMinutes = end.getHours() * 60 + end.getMinutes(); const duration = differenceInMinutes(end, start); resizePreviewTop = minutesToPercent(startMinutes); resizePreviewHeight = (duration / (totalVisibleHours * 60)) * 100; + // Calculate offset between snapped click position and actual event boundary + const clickMinutes = getMinutesFromY(e.clientY); + if (edge === 'top') { + resizeOffsetMinutes = clickMinutes - startMinutes; + } else { + resizeOffsetMinutes = clickMinutes - endMinutes; + } + document.addEventListener('pointermove', handleResizeMove); document.addEventListener('pointerup', handleResizeEnd); } @@ -451,6 +461,8 @@ hasMoved = true; const currentMinutes = getMinutesFromY(e.clientY); + // Apply offset to prevent jumping when drag starts + const adjustedMinutes = currentMinutes - resizeOffsetMinutes; const originalStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); @@ -459,7 +471,7 @@ // Resize from bottom - change end time const newEndMinutes = Math.max( originalStartMinutes + 15, - Math.min(lastVisibleHour * 60, currentMinutes) + Math.min(lastVisibleHour * 60, adjustedMinutes) ); const newDuration = newEndMinutes - originalStartMinutes; resizePreviewHeight = (newDuration / (totalVisibleHours * 60)) * 100; @@ -467,7 +479,7 @@ // Resize from top - change start time const newStartMinutes = Math.max( firstVisibleHour * 60, - Math.min(originalEndMinutes - 15, currentMinutes) + Math.min(originalEndMinutes - 15, adjustedMinutes) ); const newDuration = originalEndMinutes - newStartMinutes; resizePreviewTop = minutesToPercent(newStartMinutes); @@ -484,11 +496,14 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; return; } const currentMinutes = getMinutesFromY(e.clientY); + // Apply offset to prevent jumping + const adjustedMinutes = currentMinutes - resizeOffsetMinutes; const originalStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); @@ -499,7 +514,7 @@ if (resizeEdge === 'bottom') { const newEndMinutes = Math.max( originalStartMinutes + 15, - Math.min(lastVisibleHour * 60, currentMinutes) + Math.min(lastVisibleHour * 60, adjustedMinutes) ); const newHours = Math.floor(newEndMinutes / 60); const newMins = newEndMinutes % 60; @@ -508,7 +523,7 @@ } else { const newStartMinutes = Math.max( firstVisibleHour * 60, - Math.min(originalEndMinutes - 15, currentMinutes) + Math.min(originalEndMinutes - 15, adjustedMinutes) ); const newHours = Math.floor(newStartMinutes / 60); const newMins = newStartMinutes % 60; @@ -534,6 +549,7 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; } @@ -812,6 +828,7 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; } // Cancel task drag/resize