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 90e885ba9..fb4151272 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte @@ -19,15 +19,18 @@ const HOUR_HEIGHT = 60; // pixels per hour const SNAP_MINUTES = 15; // snap to 15-minute intervals - // Generate hours (0-23 or 7-23 depending on setting) + // Generate hours (filtered based on settings) let allHours = Array.from({ length: 24 }, (_, i) => i); let hours = $derived( - settingsStore.hideEarlyHours ? allHours.filter((h) => h >= 7) : allHours + settingsStore.filterHoursEnabled + ? allHours.filter((h) => h >= settingsStore.dayStartHour && h < settingsStore.dayEndHour) + : allHours ); // Calculate visible hours range for positioning - let firstVisibleHour = $derived(settingsStore.hideEarlyHours ? 7 : 0); - let totalVisibleHours = $derived(24 - firstVisibleHour); + let firstVisibleHour = $derived(settingsStore.filterHoursEnabled ? settingsStore.dayStartHour : 0); + let lastVisibleHour = $derived(settingsStore.filterHoursEnabled ? settingsStore.dayEndHour : 24); + let totalVisibleHours = $derived(lastVisibleHour - firstVisibleHour); // Helper to convert minutes to percentage position (accounting for hidden hours) function minutesToPercent(minutes: number): number { @@ -58,6 +61,20 @@ eventsStore.getEventsForDay(viewStore.currentDate).filter((e) => e.isAllDay) ); + // Get display mode for an event (per-event override takes precedence over global setting) + function getEventDisplayMode(event: any): 'header' | 'block' { + return event.metadata?.allDayDisplayMode || settingsStore.allDayDisplayMode; + } + + // Split all-day events by display mode + let headerAllDayEvents = $derived( + allDayEvents.filter(e => getEventDisplayMode(e) === 'header') + ); + + let blockAllDayEvents = $derived( + allDayEvents.filter(e => getEventDisplayMode(e) === 'block') + ); + // ============================================================================ // Drag & Drop State // ============================================================================ @@ -79,6 +96,9 @@ let resizePreviewTop = $state(0); let resizePreviewHeight = $state(0); + // Track if we actually moved during drag/resize (to prevent click on simple mousedown/up) + let hasMoved = $state(false); + // ============================================================================ // Helper Functions // ============================================================================ @@ -115,6 +135,7 @@ isDragging = true; draggedEvent = event; + hasMoved = false; dragPreviewTop = minutesToPercent(startMinutes); dragPreviewHeight = (duration / (totalVisibleHours * 60)) * 100; @@ -125,22 +146,23 @@ function handleDragMove(e: PointerEvent) { if (!isDragging || !draggedEvent) return; + hasMoved = true; const mouseMinutes = getMinutesFromY(e.clientY); const newStartMinutes = snapToGrid(mouseMinutes - dragOffsetMinutes); - const clampedMinutes = Math.max(firstVisibleHour * 60, Math.min(newStartMinutes, 24 * 60 - 15)); + const clampedMinutes = Math.max(firstVisibleHour * 60, Math.min(newStartMinutes, lastVisibleHour * 60 - 15)); dragPreviewTop = minutesToPercent(clampedMinutes); } function handleDragEnd(e: PointerEvent) { - if (!isDragging || !draggedEvent) { + if (!isDragging || !draggedEvent || !hasMoved) { cleanup(); return; } const mouseMinutes = getMinutesFromY(e.clientY); const newStartMinutes = snapToGrid(mouseMinutes - dragOffsetMinutes); - const clampedMinutes = Math.max(0, Math.min(newStartMinutes, 24 * 60 - 30)); + const clampedMinutes = Math.max(firstVisibleHour * 60, Math.min(newStartMinutes, lastVisibleHour * 60 - 30)); const start = typeof draggedEvent.startTime === 'string' ? parseISO(draggedEvent.startTime) : draggedEvent.startTime; const end = typeof draggedEvent.endTime === 'string' ? parseISO(draggedEvent.endTime) : draggedEvent.endTime; @@ -173,6 +195,7 @@ isResizing = true; resizeEvent = event; resizeEdge = edge; + hasMoved = false; const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime; const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime; @@ -191,6 +214,7 @@ function handleResizeMove(e: PointerEvent) { if (!isResizing || !resizeEvent || !resizeOriginalStart || !resizeOriginalEnd) return; + hasMoved = true; const mouseMinutes = getMinutesFromY(e.clientY); const snappedMinutes = snapToGrid(mouseMinutes); @@ -204,13 +228,13 @@ resizePreviewHeight = ((origEndMinutes - clampedStart) / (totalVisibleHours * 60)) * 100; } else { const newEndMinutes = Math.max(snappedMinutes, origStartMinutes + SNAP_MINUTES); - const clampedEnd = Math.min(24 * 60, newEndMinutes); + const clampedEnd = Math.min(lastVisibleHour * 60, newEndMinutes); resizePreviewHeight = ((clampedEnd - origStartMinutes) / (totalVisibleHours * 60)) * 100; } } function handleResizeEnd(e: PointerEvent) { - if (!isResizing || !resizeEvent || !resizeOriginalStart || !resizeOriginalEnd) { + if (!isResizing || !resizeEvent || !resizeOriginalStart || !resizeOriginalEnd || !hasMoved) { cleanup(); return; } @@ -225,12 +249,12 @@ let newEnd = new Date(resizeOriginalEnd); if (resizeEdge === 'top') { - const newStartMinutes = Math.max(0, Math.min(snappedMinutes, origEndMinutes - SNAP_MINUTES)); + const newStartMinutes = Math.max(firstVisibleHour * 60, Math.min(snappedMinutes, origEndMinutes - SNAP_MINUTES)); newStart = setHours(new Date(viewStore.currentDate), Math.floor(newStartMinutes / 60)); newStart = setMinutes(newStart, newStartMinutes % 60); newStart.setSeconds(0, 0); } else { - const newEndMinutes = Math.min(24 * 60, Math.max(snappedMinutes, origStartMinutes + SNAP_MINUTES)); + const newEndMinutes = Math.min(lastVisibleHour * 60, Math.max(snappedMinutes, origStartMinutes + SNAP_MINUTES)); newEnd = setHours(new Date(viewStore.currentDate), Math.floor(newEndMinutes / 60)); newEnd = setMinutes(newEnd, newEndMinutes % 60); newEnd.setSeconds(0, 0); @@ -251,12 +275,29 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + hasMoved = false; document.removeEventListener('pointermove', handleDragMove); document.removeEventListener('pointerup', handleDragEnd); document.removeEventListener('pointermove', handleResizeMove); document.removeEventListener('pointerup', handleResizeEnd); } + // ============================================================================ + // Keyboard Handling + // ============================================================================ + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape' && (isDragging || isResizing)) { + e.preventDefault(); + cleanup(); + } + } + + // Add global keydown listener + $effect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }); + // ============================================================================ // Event Styling // ============================================================================ @@ -277,10 +318,11 @@ } function handleEventClick(event: any, e: MouseEvent) { - // Don't navigate if dragging or resizing - if (isDragging || isResizing) { + // Don't navigate if dragging or resizing, or if we moved + if (isDragging || isResizing || hasMoved) { e.preventDefault(); e.stopPropagation(); + setTimeout(() => { hasMoved = false; }, 100); return; } goto(`/event/${event.id}`); @@ -297,14 +339,14 @@
- - {#if allDayEvents.length > 0} + + {#if headerAllDayEvents.length > 0}
Ganztägig
- {#each allDayEvents as event} + {#each headerAllDayEvents as event} {/each} - + + {#each blockAllDayEvents as event} + + {/each} + + {#each timedEvents as event} {@const isBeingDragged = isDragging && draggedEvent?.id === event.id} {@const isBeingResized = isResizing && resizeEvent?.id === event.id} @@ -395,7 +448,6 @@ .day-view { display: flex; flex-direction: column; - } .all-day-section { @@ -426,10 +478,41 @@ cursor: pointer; } + /* Block-style all-day events (displayed as full-day blocks in the grid) */ + .all-day-block-event { + position: absolute; + top: 0; + left: 4px; + right: 4px; + bottom: 0; + padding: 8px 12px; + color: white; + border: none; + border-radius: var(--radius-sm); + text-align: left; + cursor: pointer; + z-index: 0; + opacity: 0.3; + overflow: hidden; + display: flex; + align-items: flex-start; + } + + .all-day-block-event:hover { + opacity: 0.5; + } + + .all-day-block-event .event-title { + font-size: 0.875rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .time-grid { flex: 1; display: flex; - } .time-column { @@ -458,8 +541,6 @@ flex: 1; position: relative; border-left: 1px solid hsl(var(--color-border)); - /* Fixed height for percentage positioning to work */ - height: calc(24 * var(--hour-height)); } .day-column.today { @@ -499,9 +580,11 @@ .event-card.resizing { cursor: ns-resize; - opacity: 0.9; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + opacity: 0.85; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); z-index: 100; + outline: 2px dashed rgba(255, 255, 255, 0.6); + outline-offset: -2px; } /* Resize Handles */ 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 d7fd43ce1..756593ced 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte @@ -45,15 +45,18 @@ settingsStore.showOnlyWeekdays ? allDays.filter((day) => !isWeekend(day)) : allDays ); - // Generate hours (0-23 or 7-23 depending on setting) + // Generate hours (filtered based on settings) let allHours = Array.from({ length: 24 }, (_, i) => i); let hours = $derived( - settingsStore.hideEarlyHours ? allHours.filter((h) => h >= 7) : allHours + settingsStore.filterHoursEnabled + ? allHours.filter((h) => h >= settingsStore.dayStartHour && h < settingsStore.dayEndHour) + : allHours ); // Calculate visible hours range for positioning - let firstVisibleHour = $derived(settingsStore.hideEarlyHours ? 7 : 0); - let totalVisibleHours = $derived(24 - firstVisibleHour); + let firstVisibleHour = $derived(settingsStore.filterHoursEnabled ? settingsStore.dayStartHour : 0); + let lastVisibleHour = $derived(settingsStore.filterHoursEnabled ? settingsStore.dayEndHour : 24); + let totalVisibleHours = $derived(lastVisibleHour - firstVisibleHour); // Helper to convert minutes to percentage position (accounting for hidden hours) function minutesToPercent(minutes: number): number { @@ -100,6 +103,9 @@ let resizePreviewTop = $state(0); let resizePreviewHeight = $state(0); + // Track if we actually moved during drag/resize (to prevent click on simple mousedown/up) + let hasMoved = $state(false); + // Reference to the days container for position calculations let daysContainerEl: HTMLDivElement; @@ -111,6 +117,25 @@ return eventsStore.getEventsForDay(day).filter((e) => e.isAllDay); } + // Get display mode for an event (per-event override takes precedence over global setting) + function getEventDisplayMode(event: any): 'header' | 'block' { + return event.metadata?.allDayDisplayMode || settingsStore.allDayDisplayMode; + } + + // Split all-day events by display mode + function getHeaderAllDayEventsForDay(day: Date) { + return getAllDayEventsForDay(day).filter(e => getEventDisplayMode(e) === 'header'); + } + + function getBlockAllDayEventsForDay(day: Date) { + return getAllDayEventsForDay(day).filter(e => getEventDisplayMode(e) === 'block'); + } + + // Check if there are any all-day events to show in header + let hasAnyHeaderAllDayEvents = $derived( + days.some(day => getHeaderAllDayEventsForDay(day).length > 0) + ); + function getEventStyle(event: any) { const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime; const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime; @@ -132,10 +157,11 @@ } function handleEventClick(event: any, e: MouseEvent) { - // Don't navigate if we just finished dragging or resizing - if (isDragging || isResizing) { + // Don't navigate if we just finished dragging or resizing, or if we moved + if (isDragging || isResizing || hasMoved) { e.preventDefault(); e.stopPropagation(); + setTimeout(() => { hasMoved = false; }, 100); return; } goto(`/event/${event.id}`); @@ -186,6 +212,7 @@ isDragging = true; draggedEvent = event; + hasMoved = false; const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime; const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime; @@ -208,12 +235,14 @@ function handleDragMove(e: PointerEvent) { if (!isDragging || !draggedEvent) return; + hasMoved = true; + // Calculate new position const newDay = getDayFromX(e.clientX); const newMinutes = getMinutesFromY(e.clientY) - dragOffsetMinutes; - // Clamp to valid range (firstVisibleHour to 23:45) - const clampedMinutes = Math.max(firstVisibleHour * 60, Math.min(24 * 60 - 15, newMinutes)); + // Clamp to valid range (firstVisibleHour to lastVisibleHour) + const clampedMinutes = Math.max(firstVisibleHour * 60, Math.min(lastVisibleHour * 60 - 15, newMinutes)); // Update preview dragPreviewTop = minutesToPercent(clampedMinutes); @@ -226,9 +255,10 @@ document.removeEventListener('pointermove', handleDragMove); document.removeEventListener('pointerup', handleDragEnd); - if (!isDragging || !draggedEvent || !dragTargetDay) { + if (!isDragging || !draggedEvent || !dragTargetDay || !hasMoved) { isDragging = false; draggedEvent = null; + hasMoved = false; return; } @@ -258,6 +288,7 @@ isDragging = false; draggedEvent = null; dragTargetDay = null; + hasMoved = false; } // ========== Resize Functions ========== @@ -269,6 +300,7 @@ isResizing = true; resizeEvent = event; resizeEdge = edge; + hasMoved = false; const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime; const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime; @@ -289,13 +321,14 @@ function handleResizeMove(e: PointerEvent) { if (!isResizing || !resizeEvent || !resizeOriginalStart || !resizeOriginalEnd) return; + hasMoved = true; const currentMinutes = getMinutesFromY(e.clientY); const originalStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); if (resizeEdge === 'bottom') { // Resize from bottom - change end time - const newEndMinutes = Math.max(originalStartMinutes + 15, Math.min(24 * 60, currentMinutes)); + const newEndMinutes = Math.max(originalStartMinutes + 15, Math.min(lastVisibleHour * 60, currentMinutes)); const newDuration = newEndMinutes - originalStartMinutes; resizePreviewHeight = (newDuration / (totalVisibleHours * 60)) * 100; } else { @@ -311,9 +344,12 @@ document.removeEventListener('pointermove', handleResizeMove); document.removeEventListener('pointerup', handleResizeEnd); - if (!isResizing || !resizeEvent || !resizeOriginalStart || !resizeOriginalEnd) { + if (!isResizing || !resizeEvent || !resizeOriginalStart || !resizeOriginalEnd || !hasMoved) { isResizing = false; resizeEvent = null; + resizeOriginalStart = null; + resizeOriginalEnd = null; + hasMoved = false; return; } @@ -325,13 +361,13 @@ let newEnd = resizeOriginalEnd; if (resizeEdge === 'bottom') { - const newEndMinutes = Math.max(originalStartMinutes + 15, Math.min(24 * 60, currentMinutes)); + const newEndMinutes = Math.max(originalStartMinutes + 15, Math.min(lastVisibleHour * 60, currentMinutes)); const newHours = Math.floor(newEndMinutes / 60); const newMins = newEndMinutes % 60; newEnd = setHours(new Date(resizeOriginalEnd), newHours); newEnd = setMinutes(newEnd, newMins); } else { - const newStartMinutes = Math.max(0, Math.min(originalEndMinutes - 15, currentMinutes)); + const newStartMinutes = Math.max(firstVisibleHour * 60, Math.min(originalEndMinutes - 15, currentMinutes)); const newHours = Math.floor(newStartMinutes / 60); const newMins = newStartMinutes % 60; newStart = setHours(new Date(resizeOriginalStart), newHours); @@ -349,28 +385,56 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + hasMoved = false; } + + // ========== Keyboard Handling ========== + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape' && (isDragging || isResizing)) { + e.preventDefault(); + document.removeEventListener('pointermove', handleDragMove); + document.removeEventListener('pointerup', handleDragEnd); + document.removeEventListener('pointermove', handleResizeMove); + document.removeEventListener('pointerup', handleResizeEnd); + isDragging = false; + draggedEvent = null; + dragTargetDay = null; + isResizing = false; + resizeEvent = null; + resizeOriginalStart = null; + resizeOriginalEnd = null; + hasMoved = false; + } + } + + $effect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + });
- -
-
- {#each days as day} -
- {#each getAllDayEventsForDay(day) as event} - - {/each} -
- {/each} -
+ + {#if hasAnyHeaderAllDayEvents} +
+
+ {#each days as day} +
+ {#each getHeaderAllDayEventsForDay(day) as event} + + {/each} +
+ {/each} +
+ {/if}
@@ -406,7 +470,19 @@ > {/each} - + + {#each getBlockAllDayEventsForDay(day) as event (event.id)} + + {/each} + + {#each getEventsForDay(day) as event (event.id)} {@const isBeingDragged = isDragging && draggedEvent?.id === event.id} {@const isBeingResized = isResizing && resizeEvent?.id === event.id} @@ -480,8 +556,6 @@ .multi-day-view { display: flex; flex-direction: column; - - } .all-day-row { @@ -519,6 +593,50 @@ font-size: 0.65rem; } + /* Block-style all-day events (displayed as full-day blocks in the grid) */ + .all-day-block-event { + position: absolute; + top: 0; + left: 2px; + right: 2px; + bottom: 0; + padding: 4px 6px; + color: white; + border: none; + border-radius: var(--radius-sm); + text-align: left; + cursor: pointer; + z-index: 0; + opacity: 0.3; + overflow: hidden; + display: flex; + align-items: flex-start; + } + + .compact .all-day-block-event, + .very-compact .all-day-block-event { + left: 1px; + right: 1px; + padding: 2px 4px; + } + + .all-day-block-event:hover { + opacity: 0.5; + } + + .all-day-block-event .event-title { + font-size: 0.75rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .compact .all-day-block-event .event-title, + .very-compact .all-day-block-event .event-title { + font-size: 0.6rem; + } + .day-headers { display: flex; border-bottom: 1px solid hsl(var(--color-border)); @@ -589,7 +707,6 @@ .time-grid { flex: 1; display: flex; - } .time-column { @@ -672,8 +789,11 @@ } .event-card.resizing { - opacity: 0.9; + opacity: 0.85; z-index: 100; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); + outline: 2px dashed rgba(255, 255, 255, 0.6); + outline-offset: -2px; } .event-card.drag-ghost { 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 c34d9750a..15849819a 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte @@ -4,6 +4,7 @@ import { calendarsStore } from '$lib/stores/calendars.svelte'; import { settingsStore } from '$lib/stores/settings.svelte'; import { goto } from '$app/navigation'; + import QuickEventOverlay from '$lib/components/event/QuickEventOverlay.svelte'; import { format, eachDayOfInterval, @@ -46,15 +47,18 @@ getWeek(viewStore.viewRange.start, { weekStartsOn: settingsStore.weekStartsOn }) ); - // Generate hours (0-23 or 7-23 depending on setting) + // Generate hours (filtered based on settings) let allHours = Array.from({ length: 24 }, (_, i) => i); let hours = $derived( - settingsStore.hideEarlyHours ? allHours.filter((h) => h >= 7) : allHours + settingsStore.filterHoursEnabled + ? allHours.filter((h) => h >= settingsStore.dayStartHour && h < settingsStore.dayEndHour) + : allHours ); // Calculate visible hours range for positioning - let firstVisibleHour = $derived(settingsStore.hideEarlyHours ? 7 : 0); - let totalVisibleHours = $derived(24 - firstVisibleHour); + let firstVisibleHour = $derived(settingsStore.filterHoursEnabled ? settingsStore.dayStartHour : 0); + let lastVisibleHour = $derived(settingsStore.filterHoursEnabled ? settingsStore.dayEndHour : 24); + let totalVisibleHours = $derived(lastVisibleHour - firstVisibleHour); // Helper to convert minutes to percentage position (accounting for hidden hours) function minutesToPercent(minutes: number): number { @@ -94,6 +98,14 @@ let resizePreviewTop = $state(0); let resizePreviewHeight = $state(0); + // Track if we actually moved during drag/resize (to prevent click on simple mousedown/up) + let hasMoved = $state(false); + + // Quick Event Overlay State + let showQuickEvent = $state(false); + let quickEventStartTime = $state(null); + let quickEventPosition = $state({ x: 0, y: 0 }); + // Reference to the days container for position calculations let daysContainerEl: HTMLDivElement; @@ -105,6 +117,25 @@ return eventsStore.getEventsForDay(day).filter((e) => e.isAllDay); } + // Get display mode for an event (per-event override takes precedence over global setting) + function getEventDisplayMode(event: any): 'header' | 'block' { + return event.metadata?.allDayDisplayMode || settingsStore.allDayDisplayMode; + } + + // Split all-day events by display mode + function getHeaderAllDayEventsForDay(day: Date) { + return getAllDayEventsForDay(day).filter(e => getEventDisplayMode(e) === 'header'); + } + + function getBlockAllDayEventsForDay(day: Date) { + return getAllDayEventsForDay(day).filter(e => getEventDisplayMode(e) === 'block'); + } + + // Check if there are any all-day events to show in header + let hasAnyHeaderAllDayEvents = $derived( + days.some(day => getHeaderAllDayEventsForDay(day).length > 0) + ); + function getEventStyle(event: any) { const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime; const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime; @@ -126,22 +157,33 @@ } function handleEventClick(event: any, e: MouseEvent) { - // Don't navigate if we just finished dragging or resizing - if (isDragging || isResizing) { + // Don't navigate if we just finished dragging or resizing, or if we moved + if (isDragging || isResizing || hasMoved) { e.preventDefault(); e.stopPropagation(); + // Reset hasMoved after a short delay to allow for the next clean click + setTimeout(() => { hasMoved = false; }, 100); return; } goto(`/event/${event.id}`); } - function handleSlotClick(day: Date, hour: number) { + function handleSlotClick(day: Date, hour: number, e: MouseEvent) { // Don't create new event if dragging if (isDragging || isResizing) return; const startTime = new Date(day); startTime.setHours(hour, 0, 0, 0); - goto(`/event/new?start=${startTime.toISOString()}`); + + // Show quick event overlay at click position + quickEventStartTime = startTime; + quickEventPosition = { x: e.clientX, y: e.clientY }; + showQuickEvent = true; + } + + function closeQuickEvent() { + showQuickEvent = false; + quickEventStartTime = null; } // ========== Drag & Drop Functions ========== @@ -180,6 +222,7 @@ isDragging = true; draggedEvent = event; + hasMoved = false; const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime; const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime; @@ -202,12 +245,14 @@ function handleDragMove(e: PointerEvent) { if (!isDragging || !draggedEvent) return; + hasMoved = true; + // Calculate new position const newDay = getDayFromX(e.clientX); const newMinutes = getMinutesFromY(e.clientY) - dragOffsetMinutes; - // Clamp to valid range (firstVisibleHour to 23:45) - const clampedMinutes = Math.max(firstVisibleHour * 60, Math.min(24 * 60 - 15, newMinutes)); + // Clamp to valid range (firstVisibleHour to lastVisibleHour) + const clampedMinutes = Math.max(firstVisibleHour * 60, Math.min(lastVisibleHour * 60 - 15, newMinutes)); // Update preview dragPreviewTop = minutesToPercent(clampedMinutes); @@ -220,9 +265,10 @@ document.removeEventListener('pointermove', handleDragMove); document.removeEventListener('pointerup', handleDragEnd); - if (!isDragging || !draggedEvent || !dragTargetDay) { + if (!isDragging || !draggedEvent || !dragTargetDay || !hasMoved) { isDragging = false; draggedEvent = null; + hasMoved = false; return; } @@ -252,6 +298,7 @@ isDragging = false; draggedEvent = null; dragTargetDay = null; + hasMoved = false; } // ========== Resize Functions ========== @@ -263,6 +310,7 @@ isResizing = true; resizeEvent = event; resizeEdge = edge; + hasMoved = false; const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime; const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime; @@ -283,13 +331,14 @@ function handleResizeMove(e: PointerEvent) { if (!isResizing || !resizeEvent || !resizeOriginalStart || !resizeOriginalEnd) return; + hasMoved = true; const currentMinutes = getMinutesFromY(e.clientY); const originalStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); if (resizeEdge === 'bottom') { // Resize from bottom - change end time - const newEndMinutes = Math.max(originalStartMinutes + 15, Math.min(24 * 60, currentMinutes)); + const newEndMinutes = Math.max(originalStartMinutes + 15, Math.min(lastVisibleHour * 60, currentMinutes)); const newDuration = newEndMinutes - originalStartMinutes; resizePreviewHeight = (newDuration / (totalVisibleHours * 60)) * 100; } else { @@ -305,9 +354,12 @@ document.removeEventListener('pointermove', handleResizeMove); document.removeEventListener('pointerup', handleResizeEnd); - if (!isResizing || !resizeEvent || !resizeOriginalStart || !resizeOriginalEnd) { + if (!isResizing || !resizeEvent || !resizeOriginalStart || !resizeOriginalEnd || !hasMoved) { isResizing = false; resizeEvent = null; + resizeOriginalStart = null; + resizeOriginalEnd = null; + hasMoved = false; return; } @@ -319,13 +371,13 @@ let newEnd = resizeOriginalEnd; if (resizeEdge === 'bottom') { - const newEndMinutes = Math.max(originalStartMinutes + 15, Math.min(24 * 60, currentMinutes)); + const newEndMinutes = Math.max(originalStartMinutes + 15, Math.min(lastVisibleHour * 60, currentMinutes)); const newHours = Math.floor(newEndMinutes / 60); const newMins = newEndMinutes % 60; newEnd = setHours(new Date(resizeOriginalEnd), newHours); newEnd = setMinutes(newEnd, newMins); } else { - const newStartMinutes = Math.max(0, Math.min(originalEndMinutes - 15, currentMinutes)); + const newStartMinutes = Math.max(firstVisibleHour * 60, Math.min(originalEndMinutes - 15, currentMinutes)); const newHours = Math.floor(newStartMinutes / 60); const newMins = newStartMinutes % 60; newStart = setHours(new Date(resizeOriginalStart), newHours); @@ -343,7 +395,37 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + hasMoved = false; } + + // ========== Keyboard Handling ========== + + function handleKeyDown(e: KeyboardEvent) { + // Cancel drag/resize on Escape + if (e.key === 'Escape') { + if (isDragging || isResizing) { + e.preventDefault(); + document.removeEventListener('pointermove', handleDragMove); + document.removeEventListener('pointerup', handleDragEnd); + document.removeEventListener('pointermove', handleResizeMove); + document.removeEventListener('pointerup', handleResizeEnd); + isDragging = false; + draggedEvent = null; + dragTargetDay = null; + isResizing = false; + resizeEvent = null; + resizeOriginalStart = null; + resizeOriginalEnd = null; + hasMoved = false; + } + } + } + + // Add global keydown listener + $effect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + });
@@ -354,27 +436,29 @@
{/if} - -
-
- {#if settingsStore.showWeekNumbers} - KW {weekNumber} - {/if} -
- {#each days as day} -
- {#each getAllDayEventsForDay(day) as event} - - {/each} + + {#if hasAnyHeaderAllDayEvents} +
+
+ {#if settingsStore.showWeekNumbers} + KW {weekNumber} + {/if}
- {/each} -
+ {#each days as day} +
+ {#each getHeaderAllDayEventsForDay(day) as event} + + {/each} +
+ {/each} +
+ {/if}
@@ -405,12 +489,23 @@ {#each hours as hour} {/each} - + + {#each getBlockAllDayEventsForDay(day) as event (event.id)} + + {/each} + + {#each getEventsForDay(day) as event (event.id)} {@const isBeingDragged = isDragging && draggedEvent?.id === event.id} {@const isBeingResized = isResizing && resizeEvent?.id === event.id} @@ -479,8 +574,6 @@ .week-view { display: flex; flex-direction: column; - - } .week-number-indicator { @@ -514,6 +607,38 @@ cursor: pointer; } + /* Block-style all-day events (displayed as full-day blocks in the grid) */ + .all-day-block-event { + position: absolute; + top: 0; + left: 2px; + right: 2px; + bottom: 0; + padding: 4px 6px; + color: white; + border: none; + border-radius: var(--radius-sm); + text-align: left; + cursor: pointer; + z-index: 0; + opacity: 0.3; + overflow: hidden; + display: flex; + align-items: flex-start; + } + + .all-day-block-event:hover { + opacity: 0.5; + } + + .all-day-block-event .event-title { + font-size: 0.75rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .day-headers { display: flex; border-bottom: 1px solid hsl(var(--color-border)); @@ -568,8 +693,6 @@ .time-grid { flex: 1; display: flex; - - } .time-column { @@ -645,8 +768,11 @@ } .event-card.resizing { - opacity: 0.9; + opacity: 0.85; z-index: 100; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); + outline: 2px dashed rgba(255, 255, 255, 0.6); + outline-offset: -2px; } .event-card.drag-ghost {