From 1e5b3166b21c422303502fffa3c09814d1332525 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Wed, 8 Oct 2025 21:43:02 +0200 Subject: [PATCH] Improves event resizing with smooth animation Replaces the previous rough event resizing implementation with a smooth, animated approach. Uses pointer events for accurate tracking and adds a visual resize handle for better user interaction. Also refactors drag and drop to exclude resize handle. --- .workbench/anotherresize.txt | 179 +++++++++++++ src/managers/DragDropManager.ts | 15 +- src/managers/ResizeHandleManager.ts | 384 ++++++++++++++-------------- wwwroot/css/calendar-events-css.css | 72 +++--- 4 files changed, 420 insertions(+), 230 deletions(-) create mode 100644 .workbench/anotherresize.txt diff --git a/.workbench/anotherresize.txt b/.workbench/anotherresize.txt new file mode 100644 index 0000000..01b7b0f --- /dev/null +++ b/.workbench/anotherresize.txt @@ -0,0 +1,179 @@ +import { eventBus } from '../core/EventBus'; +import { CoreEvents } from '../constants/CoreEvents'; +import { calendarConfig } from '../core/CalendarConfig'; + +type SwpEventEl = HTMLElement & { updateHeight?: (h: number) => void }; + +export class ResizeHandleManager { + private cachedEvents: SwpEventEl[] = []; + private isResizing = false; + private targetEl: SwpEventEl | null = null; + + private startY = 0; + private startDurationMin = 0; + private direction: 'grow' | 'shrink' = 'grow'; + + private hourHeightPx: number; + private snapMin: number; + private minDurationMin: number; + private animationId: number | null = null; + private currentHeight = 0; + private targetHeight = 0; + + // cleanup + private unsubscribers: Array<() => void> = []; + private pointerCaptured = false; + private prevZ?: string; + + constructor() { + const grid = calendarConfig.getGridSettings(); + this.hourHeightPx = grid.hourHeight; + this.snapMin = grid.snapInterval; + this.minDurationMin = grid.minEventDuration ?? this.snapMin; + } + + public initialize(): void { + this.refreshEventCache(); + this.attachGlobalListeners(); + this.attachHandles(); + this.subToBus(); + } + + public destroy(): void { + document.removeEventListener('pointerdown', this.onPointerDown, true); + document.removeEventListener('pointermove', this.onPointerMove, true); + document.removeEventListener('pointerup', this.onPointerUp, true); + this.unsubscribers.forEach(u => u()); + } + + private minutesPerPx(): number { + return 60 / this.hourHeightPx; + } + + private pxFromMinutes(min: number): number { + return (min / 60) * this.hourHeightPx; + } + + private roundSnap(min: number, dir: 'grow' | 'shrink'): number { + const q = min / this.snapMin; + return (dir === 'grow' ? Math.ceil(q) : Math.floor(q)) * this.snapMin; + } + + private refreshEventCache(): void { + this.cachedEvents = Array.from( + document.querySelectorAll('swp-day-columns swp-event') + ); + } + + private attachHandles(): void { + // ensure a single handle per event + this.cachedEvents.forEach(el => { + if (!el.querySelector(':scope > swp-resize-handle')) { + const handle = document.createElement('swp-resize-handle'); + handle.setAttribute('aria-label', 'Resize event'); + handle.setAttribute('role', 'separator'); + el.appendChild(handle); + } + }); + } + + private attachGlobalListeners(): void { + document.addEventListener('pointerdown', this.onPointerDown, true); + document.addEventListener('pointermove', this.onPointerMove, true); + document.addEventListener('pointerup', this.onPointerUp, true); + } + + private subToBus(): void { + const sub = (ev: string, fn: () => void) => { + eventBus.on(ev, fn); + this.unsubscribers.push(() => eventBus.off(ev, fn)); + }; + const refresh = () => { this.refreshEventCache(); this.attachHandles(); }; + [CoreEvents.GRID_RENDERED, CoreEvents.EVENTS_RENDERED, + CoreEvents.EVENT_CREATED, CoreEvents.EVENT_UPDATED, + CoreEvents.EVENT_DELETED].forEach(ev => sub(ev, refresh)); + } + + private onPointerDown = (e: PointerEvent) => { + const handle = (e.target as HTMLElement).closest('swp-resize-handle'); + if (!handle) return; + + const el = handle.parentElement as SwpEventEl; + this.targetEl = el; + this.isResizing = true; + this.startY = e.clientY; + + // udled start-varighed fra højde + const startHeight = el.offsetHeight; + this.startDurationMin = Math.max( + this.minDurationMin, + Math.round(startHeight * this.minutesPerPx()) + ); + + this.prevZ = (el.closest('swp-event-group') ?? el).style.zIndex; + (el.closest('swp-event-group') ?? el).style.zIndex = '1000'; + + (e.target as Element).setPointerCapture?.(e.pointerId); + this.pointerCaptured = true; + document.documentElement.classList.add('swp--resizing'); // fx user-select: none; cursor: ns-resize + e.preventDefault(); + }; + + private onPointerMove = (e: PointerEvent) => { + if (!this.isResizing || !this.targetEl) return; + + const dy = e.clientY - this.startY; + this.direction = dy >= 0 ? 'grow' : 'shrink'; + + const deltaMin = dy * this.minutesPerPx(); + const rawMin = this.startDurationMin + deltaMin; + const clamped = Math.max(this.minDurationMin, rawMin); + const snappedMin = this.roundSnap(clamped, this.direction); + + this.targetHeight = this.pxFromMinutes(snappedMin); + + if (this.animationId == null) { + this.currentHeight = this.targetEl.offsetHeight; + this.animate(); + } + }; + + private animate = () => { + if (!this.isResizing || !this.targetEl) { this.animationId = null; return; } + + const diff = this.targetHeight - this.currentHeight; + if (Math.abs(diff) > 0.5) { + this.currentHeight += diff * 0.35; + this.targetEl.updateHeight?.(this.currentHeight); + this.animationId = requestAnimationFrame(this.animate); + } else { + this.currentHeight = this.targetHeight; + this.targetEl.updateHeight?.(this.currentHeight); + this.animationId = null; + } + }; + + private onPointerUp = (e: PointerEvent) => { + if (!this.isResizing || !this.targetEl) return; + + if (this.animationId != null) cancelAnimationFrame(this.animationId); + this.animationId = null; + + // sikker slut-snap + this.targetEl.updateHeight?.(this.targetHeight - 3); // lille gap til grid-linjer + + const group = this.targetEl.closest('swp-event-group') ?? this.targetEl; + group.style.zIndex = this.prevZ ?? ''; + this.prevZ = undefined; + + this.isResizing = false; + this.targetEl = null; + + if (this.pointerCaptured) { + try { (e.target as Element).releasePointerCapture?.(e.pointerId); } catch {} + this.pointerCaptured = false; + } + document.documentElement.classList.remove('swp--resizing'); + this.refreshEventCache(); + }; +} diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index b0d82ae..2b2b6f9 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -180,18 +180,11 @@ export class DragDropManager { } - // Found an event - check if in resize zone first + // Found an event - check if clicking on resize handle first if (eventElement) { - // Check if click is in bottom resize zone - if (eventElement.tagName === 'SWP-EVENT') { - const rect = eventElement.getBoundingClientRect(); - const mouseY = event.clientY; - const distanceFromBottom = rect.bottom - mouseY; - const resizeZoneHeight = 15; // Match ResizeHandleManager - // If in resize zone, don't handle this - let ResizeHandleManager take over - if (distanceFromBottom >= 0 && distanceFromBottom <= resizeZoneHeight) { - return; // Exit early - this is a resize operation - } + // Check if click is on resize handle + if (target.closest('swp-resize-handle')) { + return; // Exit early - this is a resize operation, let ResizeHandleManager handle it } // Normal drag - prepare for potential dragging this.draggedElement = eventElement; diff --git a/src/managers/ResizeHandleManager.ts b/src/managers/ResizeHandleManager.ts index 6f12d75..62944c2 100644 --- a/src/managers/ResizeHandleManager.ts +++ b/src/managers/ResizeHandleManager.ts @@ -2,234 +2,248 @@ import { eventBus } from '../core/EventBus'; import { CoreEvents } from '../constants/CoreEvents'; import { calendarConfig } from '../core/CalendarConfig'; +type SwpEventEl = HTMLElement & { updateHeight?: (h: number) => void }; + export class ResizeHandleManager { - private resizeZoneHeight = 15; // Must match CSS ::after height - private cachedEvents: HTMLElement[] = []; - - // Resize state + private cachedEvents: SwpEventEl[] = []; private isResizing = false; - private resizingElement: HTMLElement | null = null; - private initialHeight = 0; - private initialMouseY = 0; - private targetHeight = 0; - private currentHeight = 0; - private animationFrameId: number | null = null; + private targetEl: SwpEventEl | null = null; + + // Resize zone tracking (like DragDropManager hover tracking) + private isResizeZoneTrackingActive = false; + private currentTrackedEvent: SwpEventEl | null = null; + + private startY = 0; + private startDurationMin = 0; + private direction: 'grow' | 'shrink' = 'grow'; - // Snap configuration - private snapIntervalMinutes = 15; private hourHeightPx: number; + private snapMin: number; + private minDurationMin: number; + private animationId: number | null = null; + private currentHeight = 0; + private targetHeight = 0; + + // cleanup + private unsubscribers: Array<() => void> = []; + private pointerCaptured = false; + private prevZ?: string; constructor() { - const gridSettings = calendarConfig.getGridSettings(); - this.hourHeightPx = gridSettings.hourHeight; - this.snapIntervalMinutes = gridSettings.snapInterval; + const grid = calendarConfig.getGridSettings(); + this.hourHeightPx = grid.hourHeight; + this.snapMin = grid.snapInterval; + this.minDurationMin = grid.minEventDuration ?? this.snapMin; } public initialize(): void { this.refreshEventCache(); - this.setupEventListeners(); + this.attachHandles(); + this.attachGlobalListeners(); + this.subToBus(); + } + + public destroy(): void { + document.removeEventListener('pointerdown', this.onPointerDown, true); + document.removeEventListener('pointermove', this.onPointerMove, true); + document.removeEventListener('pointerup', this.onPointerUp, true); + this.unsubscribers.forEach(u => u()); + } + + private minutesPerPx(): number { + return 60 / this.hourHeightPx; + } + + private pxFromMinutes(min: number): number { + return (min / 60) * this.hourHeightPx; + } + + private roundSnap(min: number, dir: 'grow' | 'shrink'): number { + const q = min / this.snapMin; + return (dir === 'grow' ? Math.ceil(q) : Math.floor(q)) * this.snapMin; } private refreshEventCache(): void { this.cachedEvents = Array.from( - document.querySelectorAll('swp-day-columns swp-event') + document.querySelectorAll('swp-day-columns swp-event') ); } - private setupEventListeners(): void { - // Hover detection (only when not resizing and mouse button is up) - document.addEventListener('mousemove', (e: MouseEvent) => { - if (!this.isResizing) { - // Only check for resize zones when mouse button is up - if (e.buttons === 0) { - this.handleGlobalMouseMove(e); + private attachHandles(): void { + // ensure a single handle per event + this.cachedEvents.forEach(el => { + if (!el.querySelector(':scope > swp-resize-handle')) { + const handle = document.createElement('swp-resize-handle'); + handle.setAttribute('aria-label', 'Resize event'); + handle.setAttribute('role', 'separator'); + el.appendChild(handle); + } + }); + } + + private attachGlobalListeners(): void { + // Use same pattern as DragDropManager - mouseenter to activate tracking + const calendarContainer = document.querySelector('swp-calendar-container'); + + if (calendarContainer) { + calendarContainer.addEventListener('mouseenter', (e) => { + const target = e.target as HTMLElement; + const eventElement = target.closest('swp-event'); + + if (eventElement && !this.isResizing) { + this.isResizeZoneTrackingActive = true; + this.currentTrackedEvent = eventElement; } - } else { - this.handleMouseMove(e); - } - }); - - // Resize mouse handling - document.addEventListener('mousedown', (e: MouseEvent) => { - this.handleMouseDown(e); - }); - - document.addEventListener('mouseup', (e: MouseEvent) => { - this.handleMouseUp(e); - }); - - // Cache refresh - eventBus.on(CoreEvents.GRID_RENDERED, () => this.refreshEventCache()); - eventBus.on(CoreEvents.EVENTS_RENDERED, () => this.refreshEventCache()); - eventBus.on(CoreEvents.EVENT_CREATED, () => this.refreshEventCache()); - eventBus.on(CoreEvents.EVENT_UPDATED, () => this.refreshEventCache()); - eventBus.on(CoreEvents.EVENT_DELETED, () => this.refreshEventCache()); - } - - private handleGlobalMouseMove(e: MouseEvent): void { - // Check all cached events to see if mouse is in their resize zone - const events = this.cachedEvents; - - events.forEach(eventElement => { - // Skip the element we're currently resizing - if (this.resizingElement === eventElement) { - return; - } - - const rect = eventElement.getBoundingClientRect(); - const mouseY = e.clientY; - const mouseX = e.clientX; - - // Check if mouse is within element bounds horizontally - const isInHorizontalBounds = mouseX >= rect.left && mouseX <= rect.right; - - // Check if mouse is in bottom resize zone of the element - const distanceFromBottom = rect.bottom - mouseY; - const isInResizeZone = distanceFromBottom >= 0 && distanceFromBottom <= this.resizeZoneHeight; - - if (isInHorizontalBounds && isInResizeZone) { - this.showResizeIndicator(eventElement); - console.log(`✅ In resize zone - bottom ${this.resizeZoneHeight}px`); - } else { - this.hideResizeIndicator(eventElement); - } - }); - } - - private showResizeIndicator(eventElement: HTMLElement): void { - // Check if indicator already exists - let indicator = eventElement.querySelector('swp-resize-indicator'); - - if (!indicator) { - indicator = document.createElement('swp-resize-indicator'); - eventElement.appendChild(indicator); + }, true); // Capture phase } - eventElement.setAttribute('data-resize-hover', 'true'); + document.addEventListener('pointerdown', this.onPointerDown, true); + document.addEventListener('pointermove', this.onPointerMove, true); + document.addEventListener('pointerup', this.onPointerUp, true); } - private hideResizeIndicator(eventElement: HTMLElement): void { - const indicator = eventElement.querySelector('swp-resize-indicator'); - if (indicator) { - indicator.remove(); - } - eventElement.removeAttribute('data-resize-hover'); + private subToBus(): void { + const sub = (ev: string, fn: () => void) => { + eventBus.on(ev, fn); + this.unsubscribers.push(() => eventBus.off(ev, fn)); + }; + const refresh = () => { this.refreshEventCache(); this.attachHandles(); }; + [CoreEvents.GRID_RENDERED, CoreEvents.EVENTS_RENDERED, + CoreEvents.EVENT_CREATED, CoreEvents.EVENT_UPDATED, + CoreEvents.EVENT_DELETED].forEach(ev => sub(ev, refresh)); } - private handleMouseDown(e: MouseEvent): void { - const target = e.target as HTMLElement; - const eventElement = target.closest('swp-event[data-resize-hover="true"]'); + private checkResizeZone(e: PointerEvent): void { + if (!this.isResizeZoneTrackingActive || !this.currentTrackedEvent || this.isResizing) return; - if (!eventElement) return; + const rect = this.currentTrackedEvent.getBoundingClientRect(); + const mouseX = e.clientX; + const mouseY = e.clientY; - // Check if click is in bottom resize zone - const rect = eventElement.getBoundingClientRect(); - const distanceFromBottom = rect.bottom - e.clientY; + // Check if mouse is still within event bounds + const isInBounds = mouseX >= rect.left && mouseX <= rect.right && + mouseY >= rect.top && mouseY <= rect.bottom; - if (distanceFromBottom >= 0 && distanceFromBottom <= this.resizeZoneHeight) { - // START RESIZE - e.stopPropagation(); // Prevent DragDropManager from handling - e.preventDefault(); - - this.isResizing = true; - this.resizingElement = eventElement; - this.initialHeight = eventElement.offsetHeight; - this.initialMouseY = e.clientY; - - // Set high z-index on event-group if exists, otherwise on event itself - const eventGroup = eventElement.closest('swp-event-group'); - if (eventGroup) { - eventGroup.style.zIndex = '1000'; - } else { - eventElement.style.zIndex = '1000'; - } - - console.log('🔄 Resize started', this.initialHeight); - } - } - - private handleMouseMove(e: MouseEvent): void { - if (!this.isResizing || !this.resizingElement) return; - - const deltaY = e.clientY - this.initialMouseY; - const rawHeight = this.initialHeight + deltaY; - - // Apply minimum height - this.targetHeight = Math.max(30, rawHeight); - - // Start animation loop if not already running - if (this.animationFrameId === null) { - this.currentHeight = this.resizingElement.offsetHeight; - this.animate(); - } - } - - private animate(): void { - if (!this.isResizing || !this.resizingElement) { - this.animationFrameId = null; + if (!isInBounds) { + // Mouse left event - deactivate tracking + this.hideResizeIndicator(this.currentTrackedEvent); + this.isResizeZoneTrackingActive = false; + this.currentTrackedEvent = null; return; } - // Smooth interpolation towards target + // Check if in resize zone (bottom 15px) + const distanceFromBottom = rect.bottom - mouseY; + const isInResizeZone = distanceFromBottom >= 0 && distanceFromBottom <= 15; + + if (isInResizeZone) { + this.showResizeIndicator(this.currentTrackedEvent); + } else { + this.hideResizeIndicator(this.currentTrackedEvent); + } + } + + private showResizeIndicator(el: SwpEventEl): void { + el.setAttribute('data-resize-hover', 'true'); + } + + private hideResizeIndicator(el: SwpEventEl): void { + el.removeAttribute('data-resize-hover'); + } + + private onPointerDown = (e: PointerEvent) => { + const handle = (e.target as HTMLElement).closest('swp-resize-handle'); + if (!handle) return; + + const el = handle.parentElement as SwpEventEl; + this.targetEl = el; + this.isResizing = true; + this.startY = e.clientY; + + // udled start-varighed fra højde + const startHeight = el.offsetHeight; + this.startDurationMin = Math.max( + this.minDurationMin, + Math.round(startHeight * this.minutesPerPx()) + ); + + this.prevZ = (el.closest('swp-event-group') ?? el).style.zIndex; + (el.closest('swp-event-group') ?? el).style.zIndex = '1000'; + + (e.target as Element).setPointerCapture?.(e.pointerId); + this.pointerCaptured = true; + document.documentElement.classList.add('swp--resizing'); + e.preventDefault(); + }; + + private onPointerMove = (e: PointerEvent) => { + // Check resize zone if not resizing + if (!this.isResizing) { + this.checkResizeZone(e); + return; + } + + // Continue with resize logic + if (!this.targetEl) return; + + const dy = e.clientY - this.startY; + this.direction = dy >= 0 ? 'grow' : 'shrink'; + + // Calculate raw height from pixel delta (no snapping - 100% smooth like drag & drop) + const startHeight = this.pxFromMinutes(this.startDurationMin); + const rawHeight = startHeight + dy; + const minHeight = this.pxFromMinutes(this.minDurationMin); + + this.targetHeight = Math.max(minHeight, rawHeight); // Raw height, no snap + + if (this.animationId == null) { + this.currentHeight = this.targetEl.offsetHeight; + this.animate(); + } + }; + + private animate = () => { + if (!this.isResizing || !this.targetEl) { this.animationId = null; return; } + const diff = this.targetHeight - this.currentHeight; - const step = diff * 0.3; // 30% of distance per frame - - // Update if difference is significant if (Math.abs(diff) > 0.5) { - this.currentHeight += step; - - const swpEvent = this.resizingElement as any; - if (swpEvent.updateHeight) { - swpEvent.updateHeight(this.currentHeight); - } - - this.animationFrameId = requestAnimationFrame(() => this.animate()); + this.currentHeight += diff * 0.35; + this.targetEl.updateHeight?.(this.currentHeight); + this.animationId = requestAnimationFrame(this.animate); } else { - // Close enough - snap to target this.currentHeight = this.targetHeight; - const swpEvent = this.resizingElement as any; - if (swpEvent.updateHeight) { - swpEvent.updateHeight(this.currentHeight); - } - this.animationFrameId = null; + this.targetEl.updateHeight?.(this.currentHeight); + this.animationId = null; } - } + }; - private handleMouseUp(e: MouseEvent): void { - if (!this.isResizing || !this.resizingElement) return; + private onPointerUp = (e: PointerEvent) => { + if (!this.isResizing || !this.targetEl) return; - // Cancel animation - if (this.animationFrameId !== null) { - cancelAnimationFrame(this.animationFrameId); - this.animationFrameId = null; - } + if (this.animationId != null) cancelAnimationFrame(this.animationId); + this.animationId = null; - // Snap to grid on mouse up - const snapDistancePx = (this.snapIntervalMinutes / 60) * this.hourHeightPx; - const currentHeight = this.resizingElement.offsetHeight; + // Snap to grid on pointer up (like DragDropManager does on mouseUp) + const currentHeight = this.targetEl.offsetHeight; + const snapDistancePx = this.pxFromMinutes(this.snapMin); const snappedHeight = Math.round(currentHeight / snapDistancePx) * snapDistancePx; - const finalHeight = Math.max(30, snappedHeight) - 3; //a little gap, so it doesn't cover the horizontal time lines + const minHeight = this.pxFromMinutes(this.minDurationMin); + const finalHeight = Math.max(minHeight, snappedHeight) - 3; // lille gap til grid-linjer - const swpEvent = this.resizingElement as any; - if (swpEvent.updateHeight) { - swpEvent.updateHeight(finalHeight); - } + this.targetEl.updateHeight?.(finalHeight); - console.log('✅ Resize ended', finalHeight); + const group = this.targetEl.closest('swp-event-group') ?? this.targetEl; + group.style.zIndex = this.prevZ ?? ''; + this.prevZ = undefined; - // Clear z-index on event-group if exists, otherwise on event itself - const eventGroup = this.resizingElement.closest('swp-event-group'); - if (eventGroup) { - eventGroup.style.zIndex = ''; - } else { - this.resizingElement.style.zIndex = ''; - } - - // Cleanup state this.isResizing = false; - this.resizingElement = null; + this.targetEl = null; - // Refresh cache for future operations + if (this.pointerCaptured) { + try { (e.target as Element).releasePointerCapture?.(e.pointerId); } catch {} + this.pointerCaptured = false; + } + document.documentElement.classList.remove('swp--resizing'); this.refreshEventCache(); - } + }; } diff --git a/wwwroot/css/calendar-events-css.css b/wwwroot/css/calendar-events-css.css index abf3c61..0d11d43 100644 --- a/wwwroot/css/calendar-events-css.css +++ b/wwwroot/css/calendar-events-css.css @@ -92,45 +92,39 @@ swp-day-columns swp-event:hover { z-index: 20; } -/* Resize handle indicator - created by JavaScript */ -swp-resize-indicator { +/* Resize handle - actual draggable element */ +swp-resize-handle { position: absolute; - bottom: -4px; - left: 50%; - transform: translateX(-50%); - width: 50px; - height: 8px; - /* background set by JavaScript based on event color */ - border-radius: 4px; - z-index: 30; + bottom: 0; + left: 0; + right: 0; + height: 15px; + cursor: ns-resize; + z-index: 25; + display: flex; + align-items: center; + justify-content: center; opacity: 0; - animation: fadeIn 0.2s ease forwards; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); - border: 1px solid rgba(255, 255, 255, 0.3); + transition: opacity 150ms ease; } -/* Grip lines on handle */ -swp-resize-indicator::before { +/* Show handle on hover */ +swp-day-columns swp-event:hover swp-resize-handle, +swp-day-columns swp-event[data-resize-hover="true"] swp-resize-handle { + opacity: 1; +} + +/* Handle visual indicator (grip lines) */ +swp-resize-handle::before { content: ''; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - width: 20px; - height: 2px; - background: rgba(255, 255, 255, 0.8); - border-radius: 1px; - box-shadow: 0 -2px 0 rgba(255, 255, 255, 0.8), - 0 2px 0 rgba(255, 255, 255, 0.8); -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } + width: 30px; + height: 4px; + background: rgba(255, 255, 255, 0.9); + border-radius: 2px; + box-shadow: + 0 -2px 0 rgba(255, 255, 255, 0.9), + 0 2px 0 rgba(255, 255, 255, 0.9), + 0 0 4px rgba(0, 0, 0, 0.2); } swp-day-columns swp-event[data-resize-hover="true"] { @@ -138,6 +132,16 @@ swp-day-columns swp-event[data-resize-hover="true"] { overflow: visible; } +/* Global resizing state */ +.swp--resizing { + user-select: none !important; + cursor: ns-resize !important; +} + +.swp--resizing * { + cursor: ns-resize !important; +} + swp-day-columns swp-event-time { display: block; font-size: 0.875rem;