From 055308908510909b23818c6e58ebc393a7c19e55 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Fri, 26 Sep 2025 22:11:57 +0200 Subject: [PATCH] Improves all-day event drag and drop Refactors all-day event drag and drop handling for improved accuracy and performance. Introduces a shared `ColumnDetectionUtils` for consistent column detection. Simplifies all-day conversion during drag, placing events in row 1 and calculating the column from the target date. Implements differential updates during drag end, updating only changed events for smoother transitions. --- src/managers/AllDayManager.ts | 267 ++++++++++++++++---------- src/managers/DragDropManager.ts | 73 ++----- src/renderers/EventRendererManager.ts | 12 +- src/types/EventTypes.ts | 8 + src/utils/ColumnDetectionUtils.ts | 94 +++++++++ wwwroot/css/calendar-events-css.css | 38 ++-- 6 files changed, 307 insertions(+), 185 deletions(-) create mode 100644 src/utils/ColumnDetectionUtils.ts diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index dc9db3a..8b698d2 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -4,12 +4,14 @@ import { eventBus } from '../core/EventBus'; import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig'; import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer'; import { AllDayLayoutEngine } from '../utils/AllDayLayoutEngine'; +import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { CalendarEvent } from '../types/CalendarTypes'; import { DragMouseEnterHeaderEventPayload, DragStartEventPayload, DragMoveEventPayload, - DragEndEventPayload + DragEndEventPayload, + DragColumnChangeEventPayload } from '../types/EventTypes'; import { DragOffset, MousePosition } from '../types/DragDropTypes'; @@ -20,6 +22,11 @@ import { DragOffset, MousePosition } from '../types/DragDropTypes'; export class AllDayManager { private allDayEventRenderer: AllDayEventRenderer; private layoutEngine: AllDayLayoutEngine | null = null; + + // State tracking for differential updates + private currentLayouts: Map = new Map(); + private currentAllDayEvents: CalendarEvent[] = []; + private currentWeekDates: string[] = []; constructor() { this.allDayEventRenderer = new AllDayEventRenderer(); @@ -53,10 +60,6 @@ export class AllDayManager { originalElementId: originalElement?.dataset?.eventId }); - if (cloneElement && cloneElement.classList.contains('all-day-style')) { - this.handleConvertFromAllDay(cloneElement); - } - this.checkAndAnimateAllDayHeight(); }); @@ -73,17 +76,26 @@ export class AllDayManager { this.handleDragStart(draggedElement, eventId || '', mouseOffset); }); - eventBus.on('drag:move', (event) => { - const { draggedElement, mousePosition } = (event as CustomEvent).detail; - - // Only handle for all-day events - check if original element is all-day - const isAllDayEvent = draggedElement.closest('swp-allday-container'); - if (!isAllDayEvent) return; + eventBus.on('drag:column-change', (event) => { + const { draggedElement, mousePosition } = (event as CustomEvent).detail; + + // Check if there's an all-day clone for this event const eventId = draggedElement.dataset.eventId; - const dragClone = document.querySelector(`swp-allday-container swp-event[data-event-id="clone-${eventId}"]`); + const dragClone = document.querySelector(`swp-allday-container swp-event[data-event-id="clone-${eventId}"]`) as HTMLElement; + + if (!dragClone.hasAttribute('data-allday')) { + return; + } + + + // If we find an all-day clone, handle the drag move if (dragClone) { - this.handleDragMove(dragClone as HTMLElement, mousePosition); + console.log('🔄 AllDayManager: Found all-day clone, handling drag:column-change', { + eventId, + cloneId: dragClone.dataset.eventId + }); + this.handleColumnChange(dragClone, mousePosition); } }); @@ -259,6 +271,42 @@ export class AllDayManager { }); } + /** + * Store current layouts from DOM for comparison + */ + private storeCurrentLayouts(): void { + this.currentLayouts.clear(); + const container = this.getAllDayContainer(); + if (!container) return; + + container.querySelectorAll('swp-event').forEach(element => { + const htmlElement = element as HTMLElement; + const eventId = htmlElement.dataset.eventId; + const gridArea = htmlElement.style.gridArea; + if (eventId && gridArea) { + this.currentLayouts.set(eventId, gridArea); + } + }); + + console.log('📋 AllDayManager: Stored current layouts', { + count: this.currentLayouts.size, + layouts: Array.from(this.currentLayouts.entries()) + }); + } + + /** + * Set current events and week dates (called by EventRendererManager) + */ + public setCurrentEvents(events: CalendarEvent[], weekDates: string[]): void { + this.currentAllDayEvents = events; + this.currentWeekDates = weekDates; + + console.log('📝 AllDayManager: Set current events', { + eventCount: events.length, + weekDatesCount: weekDates.length + }); + } + /** * Calculate layout for ALL all-day events using AllDayLayoutEngine * This is the correct method that processes all events together for proper overlap detection @@ -276,6 +324,10 @@ export class AllDayManager { weekDates }); + // Store current state + this.currentAllDayEvents = events; + this.currentWeekDates = weekDates; + // Initialize layout engine with provided week dates this.layoutEngine = new AllDayLayoutEngine(weekDates); @@ -313,95 +365,37 @@ export class AllDayManager { /** - * Handle conversion of timed event to all-day event using CSS styling + * Handle conversion of timed event to all-day event - SIMPLIFIED + * During drag: Place in row 1 only, calculate column from targetDate */ private handleConvertToAllDay(targetDate: string, cloneElement: HTMLElement): void { - console.log('🔄 AllDayManager: Converting to all-day using AllDayLayoutEngine', { + console.log('🔄 AllDayManager: Converting to all-day (row 1 only during drag)', { eventId: cloneElement.dataset.eventId, targetDate }); // Get all-day container, request creation if needed let allDayContainer = this.getAllDayContainer(); - if (!allDayContainer) { - console.log('🔄 AllDayManager: All-day container not found, requesting creation...'); - // Request HeaderManager to create container - eventBus.emit('header:ensure-allday-container'); - // Try again after request - allDayContainer = this.getAllDayContainer(); - if (!allDayContainer) { - console.error('All-day container still not found after creation request'); - return; - } - } + // Calculate target column from targetDate using ColumnDetectionUtils + const targetColumn = ColumnDetectionUtils.getColumnIndexFromDate(targetDate); - // Create mock event for layout calculation - const mockEvent: CalendarEvent = { - id: cloneElement.dataset.eventId || '', - title: cloneElement.dataset.title || '', - start: new Date(targetDate), - end: new Date(targetDate), - type: 'work', - allDay: true, - syncStatus: 'synced' - }; - - // Get existing all-day events from EventManager - const existingEvents = this.getExistingAllDayEvents(); - - // Add the new drag event to the array - const allEvents = [...existingEvents, mockEvent]; - - // Get actual visible dates from DOM headers (same as EventRendererManager does) - const weekDates = this.getVisibleDatesFromDOM(); - - // Calculate layout for all events including the new one - const layouts = this.calculateAllDayEventsLayout(allEvents, weekDates); - const layout = layouts.get(mockEvent.id); - - if (!layout) { - console.error('AllDayManager: No layout found for drag event', mockEvent.id); - return; - } - - // Set all properties BEFORE adding to DOM + cloneElement.removeAttribute('style'); cloneElement.classList.add('all-day-style'); - cloneElement.style.gridColumn = layout.startColumn.toString(); - cloneElement.style.gridRow = layout.row.toString(); - cloneElement.dataset.allDayDate = targetDate; - cloneElement.style.display = ''; + cloneElement.style.gridRow = '1'; + cloneElement.style.gridColumn = targetColumn.toString(); + cloneElement.dataset.allday = 'true'; // Set the all-day attribute for filtering - // NOW add to container (after all positioning is calculated) - allDayContainer.appendChild(cloneElement); + // Add to container + allDayContainer?.appendChild(cloneElement); - console.log('✅ AllDayManager: Converted to all-day style using AllDayLayoutEngine', { + console.log('✅ AllDayManager: Converted to all-day style (simple row 1)', { eventId: cloneElement.dataset.eventId, - gridColumn: layout.startColumn, - gridRow: layout.row + gridColumn: targetColumn, + gridRow: 1 }); } - /** - * Handle conversion from all-day back to timed event - */ - private handleConvertFromAllDay(cloneElement: HTMLElement): void { - console.log('🔄 AllDayManager: Converting from all-day back to timed', { - eventId: cloneElement.dataset.eventId - }); - - // Remove all-day CSS class - cloneElement.classList.remove('all-day-style'); - - // Reset grid positioning - cloneElement.style.gridColumn = ''; - cloneElement.style.gridRow = ''; - - // Remove all-day date attribute - delete cloneElement.dataset.allDayDate; - - console.log('✅ AllDayManager: Converted from all-day back to timed'); - } /** * Handle drag start for all-day events @@ -439,49 +433,114 @@ export class AllDayManager { } /** - * Handle drag move for all-day events + * Handle drag move for all-day events - SPECIALIZED FOR ALL-DAY CONTAINER */ - private handleDragMove(dragClone: HTMLElement, mousePosition: MousePosition): void { - // Calculate grid column based on mouse position - const dayHeaders = document.querySelectorAll('swp-day-header'); - let targetColumn = 1; + private handleColumnChange(dragClone: HTMLElement, mousePosition: MousePosition): void { + // Get the all-day container to understand its grid structure + const allDayContainer = this.getAllDayContainer(); + if (!allDayContainer) return; - dayHeaders.forEach((header, index) => { - const rect = header.getBoundingClientRect(); - if (mousePosition.x >= rect.left && mousePosition.x <= rect.right) { - targetColumn = index + 1; - } - }); + // Calculate target column using ColumnDetectionUtils + const targetColumn = ColumnDetectionUtils.getColumnIndexFromX(mousePosition.x); - // Update clone position + // Update clone position - ALWAYS keep in row 1 during drag + // Use simple grid positioning that matches all-day container structure dragClone.style.gridColumn = targetColumn.toString(); + dragClone.style.gridRow = '1'; // Force row 1 during drag + dragClone.style.gridArea = `1 / ${targetColumn} / 2 / ${targetColumn + 1}`; - console.log('🔄 AllDayManager: Updated drag clone position', { + console.log('🔄 AllDayManager: Updated all-day drag clone position', { eventId: dragClone.dataset.eventId, targetColumn, + gridRow: 1, + gridArea: dragClone.style.gridArea, mouseX: mousePosition.x }); } /** - * Handle drag end for all-day events + * Handle drag end for all-day events - WITH DIFFERENTIAL UPDATES */ private handleDragEnd(originalElement: HTMLElement, dragClone: HTMLElement, finalPosition: { column: string; y: number }): void { - // Normalize clone + console.log('🎯 AllDayManager: Starting drag end with differential updates', { + eventId: dragClone.dataset.eventId, + finalColumn: finalPosition.column + }); + + // 1. Store current layouts BEFORE any changes + this.storeCurrentLayouts(); + + // 2. Normalize clone ID const cloneId = dragClone.dataset.eventId; if (cloneId?.startsWith('clone-')) { dragClone.dataset.eventId = cloneId.replace('clone-', ''); } - // Remove dragging styles + // 3. Create temporary array with existing events + the dropped event + const droppedEventId = dragClone.dataset.eventId || ''; + const droppedEventDate = dragClone.dataset.allDayDate || finalPosition.column; + + const droppedEvent: CalendarEvent = { + id: droppedEventId, + title: dragClone.dataset.title || dragClone.textContent || '', + start: new Date(droppedEventDate), + end: new Date(droppedEventDate), + type: 'work', + allDay: true, + syncStatus: 'synced' + }; + + // Use current events + dropped event for calculation + const tempEvents = [...this.currentAllDayEvents, droppedEvent]; + + // 4. Calculate new layouts for ALL events + const newLayouts = this.calculateAllDayEventsLayout(tempEvents, this.currentWeekDates); + + // 5. Apply differential updates - only update events that changed + let changedCount = 0; + newLayouts.forEach((layout, eventId) => { + const oldGridArea = this.currentLayouts.get(eventId); + const newGridArea = layout.gridArea; + + if (oldGridArea !== newGridArea) { + changedCount++; + const element = document.querySelector(`[data-event-id="${eventId}"]`) as HTMLElement; + if (element) { + console.log('🔄 AllDayManager: Updating event position', { + eventId, + oldGridArea, + newGridArea + }); + + // Add transition class for smooth animation + element.classList.add('transitioning'); + element.style.gridArea = newGridArea; + element.style.gridRow = layout.row.toString(); + element.style.gridColumn = `${layout.startColumn} / ${layout.endColumn + 1}`; + + // Remove transition class after animation + setTimeout(() => element.classList.remove('transitioning'), 200); + } + } + }); + + // 6. Clean up drag styles from the dropped clone dragClone.classList.remove('dragging'); dragClone.style.zIndex = ''; dragClone.style.cursor = ''; dragClone.style.opacity = ''; - console.log('✅ AllDayManager: Completed drag operation for all-day event', { - eventId: dragClone.dataset.eventId, - finalColumn: dragClone.style.gridColumn + // 7. Restore original element opacity + originalElement.style.opacity = ''; + + // 8. Check if height adjustment is needed + this.checkAndAnimateAllDayHeight(); + + console.log('✅ AllDayManager: Completed differential drag end', { + eventId: droppedEventId, + totalEvents: newLayouts.size, + changedEvents: changedCount, + finalGridArea: newLayouts.get(droppedEventId)?.gridArea }); } diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index 8db9840..54844bf 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -6,12 +6,14 @@ import { IEventBus } from '../types/CalendarTypes'; import { calendarConfig } from '../core/CalendarConfig'; import { PositionUtils } from '../utils/PositionUtils'; +import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, - DragMouseLeaveHeaderEventPayload + DragMouseLeaveHeaderEventPayload, + DragColumnChangeEventPayload } from '../types/EventTypes'; interface CachedElements { @@ -25,11 +27,6 @@ interface Position { y: number; } -interface ColumnBounds { - date: string; - left: number; - right: number; -} export class DragDropManager { private eventBus: IEventBus; @@ -59,8 +56,6 @@ export class DragDropManager { lastColumnDate: null }; - // Column bounds cache for coordinate-based column detection - private columnBoundsCache: ColumnBounds[] = []; // Auto-scroll properties @@ -120,16 +115,16 @@ export class DragDropManager { } // Initialize column bounds cache - this.updateColumnBoundsCache(); + ColumnDetectionUtils.updateColumnBoundsCache(); // Listen to resize events to update cache window.addEventListener('resize', () => { - this.updateColumnBoundsCache(); + ColumnDetectionUtils.updateColumnBoundsCache(); }); // Listen to navigation events to update cache this.eventBus.on('navigation:completed', () => { - this.updateColumnBoundsCache(); + ColumnDetectionUtils.updateColumnBoundsCache(); }); } @@ -247,12 +242,13 @@ export class DragDropManager { const previousColumn = this.currentColumn; this.currentColumn = newColumn; - this.eventBus.emit('drag:column-change', { + const dragColumnChangePayload: DragColumnChangeEventPayload = { draggedElement: this.draggedElement, previousColumn, newColumn, mousePosition: currentPosition - }); + }; + this.eventBus.emit('drag:column-change', dragColumnChangePayload); } } } @@ -377,58 +373,13 @@ export class DragDropManager { return Math.max(0, snappedY); } - /** - * Update column bounds cache for coordinate-based column detection - */ - private updateColumnBoundsCache(): void { - // Reset cache - this.columnBoundsCache = []; - - // Find alle kolonner - const columns = document.querySelectorAll('swp-day-column'); - - // Cache hver kolonnes x-grænser - columns.forEach(column => { - const rect = column.getBoundingClientRect(); - const date = (column as HTMLElement).dataset.date; - - if (date) { - this.columnBoundsCache.push({ - date, - left: rect.left, - right: rect.right - }); - } - }); - - // Sorter efter x-position (fra venstre til højre) - this.columnBoundsCache.sort((a, b) => a.left - b.left); - - } - - /** - * Get column date from X coordinate using cached bounds - */ - private getColumnDateFromX(x: number): string | null { - // Opdater cache hvis tom - if (this.columnBoundsCache.length === 0) { - this.updateColumnBoundsCache(); - } - - // Find den kolonne hvor x-koordinaten er indenfor grænserne - const column = this.columnBoundsCache.find(col => - x >= col.left && x <= col.right - ); - - return column ? column.date : null; - } /** * Coordinate-based column detection (replaces DOM traversal) */ private detectColumn(mouseX: number, mouseY: number): string | null { // Brug den koordinatbaserede metode direkte - const columnDate = this.getColumnDateFromX(mouseX); + const columnDate = ColumnDetectionUtils.getColumnDateFromX(mouseX); // Opdater stadig den eksisterende cache hvis vi finder en kolonne if (columnDate && columnDate !== this.cachedElements.lastColumnDate) { @@ -610,7 +561,7 @@ export class DragDropManager { this.isInHeader = true; // Calculate target date using existing method - const targetDate = this.getColumnDateFromX(event.clientX); + const targetDate = ColumnDetectionUtils.getColumnDateFromX(event.clientX); if (targetDate) { console.log('🎯 DragDropManager: Emitting drag:mouseenter-header', { targetDate }); @@ -636,7 +587,7 @@ export class DragDropManager { console.log('🚪 DragDropManager: Emitting drag:mouseleave-header'); // Calculate target date using existing method - const targetDate = this.getColumnDateFromX(event.clientX); + const targetDate = ColumnDetectionUtils.getColumnDateFromX(event.clientX); // Find clone element (if it exists) const eventId = this.draggedElement?.dataset.eventId; diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 64856dd..86a8c4d 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -8,7 +8,7 @@ import { AllDayManager } from '../managers/AllDayManager'; import { EventRendererStrategy } from './EventRenderer'; import { SwpEventElement } from '../elements/SwpEventElement'; import { AllDayEventRenderer } from './AllDayEventRenderer'; -import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, HeaderReadyEventPayload } from '../types/EventTypes'; +import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, DragColumnChangeEventPayload, HeaderReadyEventPayload } from '../types/EventTypes'; /** * EventRenderingService - Render events i DOM med positionering using Strategy Pattern * Håndterer event positioning og overlap detection @@ -190,6 +190,11 @@ export class EventRenderingService { // Handle drag move this.eventBus.on('drag:move', (event: Event) => { const { draggedElement, snappedY, column, mouseOffset } = (event as CustomEvent).detail; + + // Filter: Only handle events WITHOUT data-allday attribute (normal timed events) + if (draggedElement.hasAttribute('data-allday')) { + return; // This is an all-day event, let AllDayManager handle it + } if (this.strategy.handleDragMove && column) { const eventId = draggedElement.dataset.eventId || ''; this.strategy.handleDragMove(eventId, snappedY, column, mouseOffset); @@ -241,7 +246,7 @@ export class EventRenderingService { // Handle column change this.eventBus.on('drag:column-change', (event: Event) => { - const { draggedElement, newColumn } = (event as CustomEvent).detail; + const { draggedElement, newColumn } = (event as CustomEvent).detail; if (this.strategy.handleColumnChange) { const eventId = draggedElement.dataset.eventId || ''; this.strategy.handleColumnChange(eventId, newColumn); @@ -359,6 +364,9 @@ export class EventRenderingService { count: weekDates.length }); + // Pass current events to AllDayManager for state tracking + this.allDayManager.setCurrentEvents(allDayEvents, weekDates); + // Calculate layout for ALL all-day events together using AllDayLayoutEngine const layouts = this.allDayManager.calculateAllDayEventsLayout(allDayEvents, weekDates); diff --git a/src/types/EventTypes.ts b/src/types/EventTypes.ts index f094b0a..56bbb46 100644 --- a/src/types/EventTypes.ts +++ b/src/types/EventTypes.ts @@ -87,6 +87,14 @@ export interface DragMouseLeaveHeaderEventPayload { cloneElement: HTMLElement| null; } +// Drag column change event payload +export interface DragColumnChangeEventPayload { + draggedElement: HTMLElement; + previousColumn: string | null; + newColumn: string; + mousePosition: MousePosition; +} + // Header ready event payload export interface HeaderReadyEventPayload { headerElement: HTMLElement; diff --git a/src/utils/ColumnDetectionUtils.ts b/src/utils/ColumnDetectionUtils.ts new file mode 100644 index 0000000..44fd650 --- /dev/null +++ b/src/utils/ColumnDetectionUtils.ts @@ -0,0 +1,94 @@ +/** + * ColumnDetectionUtils - Shared utility for column detection and caching + * Used by both DragDropManager and AllDayManager for consistent column detection + */ + +export interface ColumnBounds { + date: string; + left: number; + right: number; +} + +export class ColumnDetectionUtils { + private static columnBoundsCache: ColumnBounds[] = []; + + /** + * Update column bounds cache for coordinate-based column detection + */ + public static updateColumnBoundsCache(): void { + // Reset cache + this.columnBoundsCache = []; + + // Find alle kolonner + const columns = document.querySelectorAll('swp-day-column'); + + // Cache hver kolonnes x-grænser + columns.forEach(column => { + const rect = column.getBoundingClientRect(); + const date = (column as HTMLElement).dataset.date; + + if (date) { + this.columnBoundsCache.push({ + date, + left: rect.left, + right: rect.right + }); + } + }); + + // Sorter efter x-position (fra venstre til højre) + this.columnBoundsCache.sort((a, b) => a.left - b.left); + } + + /** + * Get column date from X coordinate using cached bounds + */ + public static getColumnDateFromX(x: number): string | null { + // Opdater cache hvis tom + if (this.columnBoundsCache.length === 0) { + this.updateColumnBoundsCache(); + } + + // Find den kolonne hvor x-koordinaten er indenfor grænserne + const column = this.columnBoundsCache.find(col => + x >= col.left && x <= col.right + ); + + return column ? column.date : null; + } + + /** + * Get column index (1-based) from date + */ + public static getColumnIndexFromDate(date: string): number { + // Opdater cache hvis tom + if (this.columnBoundsCache.length === 0) { + this.updateColumnBoundsCache(); + } + + const columnIndex = this.columnBoundsCache.findIndex(col => col.date === date); + return columnIndex >= 0 ? columnIndex + 1 : 1; // 1-based index + } + + /** + * Get column index from X coordinate + */ + public static getColumnIndexFromX(x: number): number { + const date = this.getColumnDateFromX(x); + return date ? this.getColumnIndexFromDate(date) : 1; + } + + /** + * Clear cache (useful for testing or when DOM structure changes) + */ + public static clearCache(): void { + this.columnBoundsCache = []; + } + + /** + * Get current cache for debugging + */ + public static getCache(): ColumnBounds[] { + return [...this.columnBoundsCache]; + } +} \ No newline at end of file diff --git a/wwwroot/css/calendar-events-css.css b/wwwroot/css/calendar-events-css.css index f7d8ac0..24fd145 100644 --- a/wwwroot/css/calendar-events-css.css +++ b/wwwroot/css/calendar-events-css.css @@ -125,24 +125,21 @@ swp-resize-handle[data-position="bottom"] { } /* Resize handles controlled by JavaScript - no general hover */ - - /* Hit area */ - swp-handle-hitarea { - position: absolute; - left: -8px; - right: -8px; - top: -6px; - bottom: -6px; - cursor: ns-resize; - } - - &[data-position="top"] { - top: 4px; - } - - &[data-position="bottom"] { - bottom: 4px; - } +swp-handle-hitarea { + position: absolute; + left: -8px; + right: -8px; + top: -6px; + bottom: -6px; + cursor: ns-resize; +} + +swp-handle-hitarea[data-position="top"] { + top: 4px; +} + +swp-handle-hitarea[data-position="bottom"] { + bottom: 4px; } /* Multi-day events */ @@ -250,3 +247,8 @@ swp-event-group swp-event { right: 0; margin: 0; } + +/* All-day event transition for smooth repositioning */ +swp-allday-container swp-event.transitioning { + transition: grid-area 200ms ease-out, grid-row 200ms ease-out, grid-column 200ms ease-out; +}