diff --git a/Calendar Plantempus.sln b/Calendar Plantempus.sln new file mode 100644 index 0000000..cad28d8 --- /dev/null +++ b/Calendar Plantempus.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalendarServer", "CalendarServer.csproj", "{012B9532-C22E-001C-4A02-5B97A6446613}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {012B9532-C22E-001C-4A02-5B97A6446613}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {012B9532-C22E-001C-4A02-5B97A6446613}.Debug|Any CPU.Build.0 = Debug|Any CPU + {012B9532-C22E-001C-4A02-5B97A6446613}.Release|Any CPU.ActiveCfg = Release|Any CPU + {012B9532-C22E-001C-4A02-5B97A6446613}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {84660DE0-41AA-4852-B51E-EBA1072CC7A4} + EndGlobalSection +EndGlobal diff --git a/src/core/CalendarConfig.ts b/src/core/CalendarConfig.ts index 93d37ab..114246c 100644 --- a/src/core/CalendarConfig.ts +++ b/src/core/CalendarConfig.ts @@ -27,8 +27,8 @@ export class CalendarConfig { firstDayOfWeek: 1, // 0 = Sunday, 1 = Monday // Time settings - dayStartHour: 7, // Calendar starts at 7 AM - dayEndHour: 19, // Calendar ends at 7 PM + dayStartHour: 0, // Calendar starts at midnight + dayEndHour: 24, // Calendar ends at midnight (24 hours) workStartHour: 8, // Work hours start workEndHour: 17, // Work hours end snapInterval: 15, // Minutes: 5, 10, 15, 30, 60 diff --git a/src/index.ts b/src/index.ts index 4c6cf39..d1caca2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { NavigationManager } from './managers/NavigationManager.js'; import { ViewManager } from './managers/ViewManager.js'; import { EventManager } from './managers/EventManager.js'; import { EventRenderer } from './managers/EventRenderer.js'; +import { GridManager } from './managers/GridManager.js'; import { CalendarConfig } from './core/CalendarConfig.js'; /** @@ -22,6 +23,7 @@ function initializeCalendar(): void { const viewManager = new ViewManager(eventBus); const eventManager = new EventManager(eventBus); const eventRenderer = new EventRenderer(eventBus); + const gridManager = new GridManager(); // Enable debug mode for development eventBus.setDebug(true); @@ -38,7 +40,8 @@ function initializeCalendar(): void { navigationManager, viewManager, eventManager, - eventRenderer + eventRenderer, + gridManager }; } diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index f14f868..cc41364 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -33,6 +33,7 @@ export class GridManager { private init(): void { this.findElements(); this.subscribeToEvents(); + this.setupScrollSync(); } private findElements(): void { @@ -64,6 +65,19 @@ export class GridManager { this.renderHeaders(); }); + // Handle week changes from NavigationManager + eventBus.on(EventTypes.WEEK_CHANGED, (e: Event) => { + const detail = (e as CustomEvent).detail; + this.currentWeek = detail.weekStart; + this.render(); + }); + + // Handle new week container creation + eventBus.on(EventTypes.WEEK_CONTAINER_CREATED, (e: Event) => { + const detail = (e as CustomEvent).detail; + this.renderGridForContainer(detail.container, detail.weekStart); + }); + // Handle grid clicks this.setupGridInteractions(); } @@ -345,4 +359,228 @@ export class GridManager { this.scrollableContent.scrollTop = scrollTop; } + + /** + * Render grid for a specific container (used during navigation transitions) + */ + private renderGridForContainer(container: HTMLElement, weekStart: Date): void { + // Find the week header and scrollable content within this container + const weekHeader = container.querySelector('swp-week-header'); + const scrollableContent = container.querySelector('swp-scrollable-content'); + const timeGrid = container.querySelector('swp-time-grid'); + + if (!weekHeader || !scrollableContent || !timeGrid) { + console.warn('GridManager: Required elements not found in container'); + return; + } + + // Render week header for this container + this.renderWeekHeaderForContainer(weekHeader as HTMLElement, weekStart); + + // Render grid content for this container - pass weekStart + this.renderGridForSpecificContainer(container, weekStart); + this.renderGridLinesForContainer(timeGrid as HTMLElement); + this.setupGridInteractionsForContainer(container); + + // Setup scroll sync for this new container + this.setupScrollSyncForContainer(scrollableContent as HTMLElement); + } + + /** + * Render week header for a specific container + */ + private renderWeekHeaderForContainer(weekHeader: HTMLElement, weekStart: Date): void { + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + + weekHeader.innerHTML = ''; + + for (let i = 0; i < 7; i++) { + const date = new Date(weekStart); + date.setDate(date.getDate() + i); + + const header = document.createElement('swp-day-header'); + if (this.isToday(date)) { + (header as any).dataset.today = 'true'; + } + + header.innerHTML = ` + ${days[date.getDay()]} + ${date.getDate()} + `; + (header as any).dataset.date = this.formatDate(date); + + weekHeader.appendChild(header); + } + } + + /** + * Render grid structure for a specific container + */ + private renderGridForSpecificContainer(container: HTMLElement, weekStart?: Date): void { + const timeGrid = container.querySelector('swp-time-grid'); + if (!timeGrid) { + console.warn('GridManager: No time-grid found in container'); + return; + } + + // Use the weekStart parameter or fall back to currentWeek + const targetWeek = weekStart || this.currentWeek; + if (!targetWeek) { + console.warn('GridManager: No target week available'); + return; + } + + // Clear existing columns + let dayColumns = timeGrid.querySelector('swp-day-columns'); + if (!dayColumns) { + dayColumns = document.createElement('swp-day-columns'); + timeGrid.appendChild(dayColumns); + } + + dayColumns.innerHTML = ''; + + const view = calendarConfig.get('view'); + const columnsCount = view === 'week' ? calendarConfig.get('weekDays') : 1; + + // Create columns using the target week + for (let i = 0; i < columnsCount; i++) { + const column = document.createElement('swp-day-column'); + (column as any).dataset.columnIndex = i; + + const dates = this.getWeekDates(targetWeek); + if (dates[i]) { + (column as any).dataset.date = this.formatDate(dates[i]); + } + + // Add events container + const eventsLayer = document.createElement('swp-events-layer'); + column.appendChild(eventsLayer); + + dayColumns.appendChild(column); + } + + // Update grid styles for this container + const totalHeight = calendarConfig.totalHours * calendarConfig.get('hourHeight'); + (timeGrid as HTMLElement).style.height = `${totalHeight}px`; + } + + /** + * Render grid lines for a specific time grid + */ + private renderGridLinesForContainer(timeGrid: HTMLElement): void { + let gridLines = timeGrid.querySelector('swp-grid-lines'); + if (!gridLines) { + gridLines = document.createElement('swp-grid-lines'); + timeGrid.insertBefore(gridLines, timeGrid.firstChild); + } + + const totalHours = calendarConfig.totalHours; + const hourHeight = calendarConfig.get('hourHeight'); + + // Set CSS variables + timeGrid.style.setProperty('--total-hours', totalHours.toString()); + timeGrid.style.setProperty('--hour-height', `${hourHeight}px`); + } + + /** + * Setup grid interactions for a specific container + */ + private setupGridInteractionsForContainer(container: HTMLElement): void { + const timeGrid = container.querySelector('swp-time-grid'); + if (!timeGrid) return; + + // Click handler + timeGrid.addEventListener('click', (e: Event) => { + const mouseEvent = e as MouseEvent; + // Ignore if clicking on an event + if ((mouseEvent.target as Element).closest('swp-event')) return; + + const column = (mouseEvent.target as Element).closest('swp-day-column') as HTMLElement; + if (!column) return; + + const position = this.getClickPositionForContainer(mouseEvent, column, container); + + eventBus.emit(EventTypes.GRID_CLICK, { + date: (column as any).dataset.date, + time: position.time, + minutes: position.minutes, + columnIndex: parseInt((column as any).dataset.columnIndex) + }); + }); + + // Double click handler + timeGrid.addEventListener('dblclick', (e: Event) => { + const mouseEvent = e as MouseEvent; + // Ignore if clicking on an event + if ((mouseEvent.target as Element).closest('swp-event')) return; + + const column = (mouseEvent.target as Element).closest('swp-day-column') as HTMLElement; + if (!column) return; + + const position = this.getClickPositionForContainer(mouseEvent, column, container); + + eventBus.emit(EventTypes.GRID_DBLCLICK, { + date: (column as any).dataset.date, + time: position.time, + minutes: position.minutes, + columnIndex: parseInt((column as any).dataset.columnIndex) + }); + }); + } + + /** + * Get click position for a specific container + */ + private getClickPositionForContainer(event: MouseEvent, column: HTMLElement, container: HTMLElement): GridPosition { + const rect = column.getBoundingClientRect(); + const scrollableContent = container.querySelector('swp-scrollable-content') as HTMLElement; + const y = event.clientY - rect.top + (scrollableContent?.scrollTop || 0); + + const minuteHeight = calendarConfig.minuteHeight; + const snapInterval = calendarConfig.get('snapInterval'); + const dayStartHour = calendarConfig.get('dayStartHour'); + + // Calculate minutes from start of day + let minutes = Math.floor(y / minuteHeight); + + // Snap to interval + minutes = Math.round(minutes / snapInterval) * snapInterval; + + // Add day start offset + const totalMinutes = (dayStartHour * 60) + minutes; + + return { + minutes: totalMinutes, + time: this.minutesToTime(totalMinutes), + y: minutes * minuteHeight + }; + } + + /** + * Setup scroll synchronization between time-axis and scrollable content + */ + private setupScrollSync(): void { + if (!this.scrollableContent || !this.timeAxis) return; + + // Sync time-axis scroll with scrollable content + this.scrollableContent.addEventListener('scroll', () => { + if (this.timeAxis) { + this.timeAxis.scrollTop = this.scrollableContent!.scrollTop; + } + }); + } + + /** + * Setup scroll synchronization for a specific container's scrollable content + */ + private setupScrollSyncForContainer(scrollableContent: HTMLElement): void { + if (!this.timeAxis) return; + + // Sync time-axis scroll with this container's scrollable content + scrollableContent.addEventListener('scroll', () => { + if (this.timeAxis) { + this.timeAxis.scrollTop = scrollableContent.scrollTop; + } + }); + } } \ No newline at end of file diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts index 6d01405..8971e8f 100644 --- a/src/managers/NavigationManager.ts +++ b/src/managers/NavigationManager.ts @@ -105,15 +105,15 @@ export class NavigationManager { } private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void { - const container = document.querySelector('swp-calendar-container'); + const calendarContainer = document.querySelector('swp-calendar-container'); const currentWeekContainer = document.querySelector('swp-week-container'); - if (!container || !currentWeekContainer) { + if (!calendarContainer || !currentWeekContainer) { console.warn('NavigationManager: Required DOM elements not found'); return; } - // Create new week container + // Create new week container (following POC structure) const newWeekContainer = document.createElement('swp-week-container'); newWeekContainer.innerHTML = ` @@ -133,8 +133,8 @@ export class NavigationManager { newWeekContainer.style.height = '100%'; newWeekContainer.style.transform = direction === 'next' ? 'translateX(100%)' : 'translateX(-100%)'; - // Add to container - container.appendChild(newWeekContainer); + // Add to calendar container + calendarContainer.appendChild(newWeekContainer); // Notify other managers to render content for the new week this.eventBus.emit(EventTypes.WEEK_CONTAINER_CREATED, { diff --git a/src/utils/PositionUtils.js b/src/utils/PositionUtils.js new file mode 100644 index 0000000..5172625 --- /dev/null +++ b/src/utils/PositionUtils.js @@ -0,0 +1,218 @@ +/** + * PositionUtils - Utility functions for converting between pixels and minutes/hours in the calendar + * This module provides essential conversion functions for positioning events and calculating dimensions + */ + +import { calendarConfig } from '../core/CalendarConfig.js'; + +export class PositionUtils { + /** + * Convert minutes to pixels based on the current time scale + * @param {number} minutes - Number of minutes to convert + * @returns {number} Pixel value + */ + static minutesToPixels(minutes) { + return minutes * calendarConfig.minuteHeight; + } + + /** + * Convert pixels to minutes based on the current time scale + * @param {number} pixels - Number of pixels to convert + * @returns {number} Minutes value + */ + static pixelsToMinutes(pixels) { + return pixels / calendarConfig.minuteHeight; + } + + /** + * Convert a time string (HH:MM) to minutes from start of day + * @param {string} timeString - Time in format "HH:MM" + * @returns {number} Minutes from start of day + */ + static timeStringToMinutes(timeString) { + const [hours, minutes] = timeString.split(':').map(Number); + return hours * 60 + minutes; + } + + /** + * Convert minutes from start of day to time string (HH:MM) + * @param {number} minutes - Minutes from start of day + * @returns {string} Time in format "HH:MM" + */ + static minutesToTimeString(minutes) { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`; + } + + /** + * Calculate the pixel position for a given time + * @param {string|number} time - Time as string "HH:MM" or minutes from start of day + * @returns {number} Pixel position from top of calendar + */ + static getPixelPositionForTime(time) { + const startHour = calendarConfig.get('dayStartHour'); + + let minutes; + if (typeof time === 'string') { + minutes = this.timeStringToMinutes(time); + } else { + minutes = time; + } + + // Subtract start hour offset + const adjustedMinutes = minutes - (startHour * 60); + + return this.minutesToPixels(adjustedMinutes); + } + + /** + * Calculate the time for a given pixel position + * @param {number} pixelPosition - Pixel position from top of calendar + * @returns {number} Minutes from start of day + */ + static getTimeForPixelPosition(pixelPosition) { + const startHour = calendarConfig.get('dayStartHour'); + + const minutes = this.pixelsToMinutes(pixelPosition); + + // Add start hour offset + return minutes + (startHour * 60); + } + + /** + * Calculate event height based on duration + * @param {number} durationMinutes - Duration in minutes + * @returns {number} Height in pixels + */ + static getEventHeight(durationMinutes) { + return this.minutesToPixels(durationMinutes); + } + + /** + * Calculate event duration based on height + * @param {number} heightPixels - Height in pixels + * @returns {number} Duration in minutes + */ + static getEventDuration(heightPixels) { + return this.pixelsToMinutes(heightPixels); + } + + /** + * Get the pixel position for a specific day column + * @param {number} dayIndex - Day index (0 = Monday, 6 = Sunday) + * @returns {number} Pixel position from left + */ + static getDayColumnPosition(dayIndex) { + // These values should be calculated based on actual calendar layout + const timeAxisWidth = 60; // Default time axis width + const calendarElement = document.querySelector('swp-calendar-content'); + const dayColumnWidth = calendarElement ? + (calendarElement.clientWidth - timeAxisWidth) / calendarConfig.get('weekDays') : + 120; // Default day column width + + return timeAxisWidth + (dayIndex * dayColumnWidth); + } + + /** + * Get the day index for a given pixel position + * @param {number} pixelPosition - Pixel position from left + * @returns {number} Day index (0-6) or -1 if outside day columns + */ + static getDayIndexForPosition(pixelPosition) { + const timeAxisWidth = 60; // Default time axis width + const calendarElement = document.querySelector('swp-calendar-content'); + const dayColumnWidth = calendarElement ? + (calendarElement.clientWidth - timeAxisWidth) / calendarConfig.get('weekDays') : + 120; // Default day column width + + if (pixelPosition < timeAxisWidth) { + return -1; // In time axis area + } + + const dayPosition = pixelPosition - timeAxisWidth; + const dayIndex = Math.floor(dayPosition / dayColumnWidth); + + return dayIndex >= 0 && dayIndex < calendarConfig.get('weekDays') ? dayIndex : -1; + } + + /** + * Calculate the bounds for an event element + * @param {Object} eventData - Event data with startTime, endTime, and day + * @returns {Object} Bounds object with top, left, width, height + */ + static getEventBounds(eventData) { + const startMinutes = typeof eventData.startTime === 'string' + ? this.timeStringToMinutes(eventData.startTime) + : eventData.startTime; + + const endMinutes = typeof eventData.endTime === 'string' + ? this.timeStringToMinutes(eventData.endTime) + : eventData.endTime; + + const duration = endMinutes - startMinutes; + + const calendarElement = document.querySelector('swp-calendar-content'); + const timeAxisWidth = 60; // Default time axis width + const dayColumnWidth = calendarElement ? + (calendarElement.clientWidth - timeAxisWidth) / calendarConfig.get('weekDays') : + 120; // Default day column width + + return { + top: this.getPixelPositionForTime(startMinutes), + left: this.getDayColumnPosition(eventData.day), + width: dayColumnWidth, + height: this.getEventHeight(duration) + }; + } + + /** + * Check if a pixel position is within the visible time range + * @param {number} pixelPosition - Pixel position from top + * @returns {boolean} True if within visible range + */ + static isWithinVisibleTimeRange(pixelPosition) { + const startHour = calendarConfig.get('dayStartHour'); + const endHour = calendarConfig.get('dayEndHour'); + + const minutes = this.getTimeForPixelPosition(pixelPosition); + const hours = minutes / 60; + + return hours >= startHour && hours <= endHour; + } + + /** + * Clamp a pixel position to the visible time range + * @param {number} pixelPosition - Pixel position from top + * @returns {number} Clamped pixel position + */ + static clampToVisibleTimeRange(pixelPosition) { + const startHour = calendarConfig.get('dayStartHour'); + const endHour = calendarConfig.get('dayEndHour'); + + const minPosition = this.getPixelPositionForTime(startHour * 60); + const maxPosition = this.getPixelPositionForTime(endHour * 60); + + return Math.max(minPosition, Math.min(maxPosition, pixelPosition)); + } + + /** + * Get the total height of the calendar content area + * @returns {number} Total height in pixels + */ + static getTotalCalendarHeight() { + return calendarConfig.get('hourHeight') * calendarConfig.totalHours; + } + + /** + * Round a pixel position to the nearest time interval + * @param {number} pixelPosition - Pixel position to round + * @param {number} intervalMinutes - Interval in minutes (default: 15) + * @returns {number} Rounded pixel position + */ + static roundToTimeInterval(pixelPosition, intervalMinutes = 15) { + const minutes = this.getTimeForPixelPosition(pixelPosition); + const roundedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes; + return this.getPixelPositionForTime(roundedMinutes); + } +} \ No newline at end of file diff --git a/wwwroot/css/calendar-base-css.css b/wwwroot/css/calendar-base-css.css index 73d2e10..b4db468 100644 --- a/wwwroot/css/calendar-base-css.css +++ b/wwwroot/css/calendar-base-css.css @@ -15,8 +15,8 @@ --snap-interval: 15; /* Time boundaries */ - --day-start-hour: 7; - --day-end-hour: 19; + --day-start-hour: 0; + --day-end-hour: 24; --work-start-hour: 8; --work-end-hour: 17; @@ -94,9 +94,26 @@ swp-day-columns, swp-day-column, swp-events-layer, swp-event, -swp-allday-container, swp-loading-overlay, -swp-event-popup { +swp-week-container, +swp-grid-lines, +swp-nav-group, +swp-nav-button, +swp-search-container, +swp-search-icon, +swp-search-clear, +swp-view-selector, +swp-view-button, +swp-week-info, +swp-week-number, +swp-date-range, +swp-day-header, +swp-day-name, +swp-day-date, +swp-hour-marker, +swp-event-time, +swp-event-title, +swp-spinner { display: block; } diff --git a/wwwroot/css/calendar-components-css.css b/wwwroot/css/calendar-components-css.css index ed97b2f..a0b6668 100644 --- a/wwwroot/css/calendar-components-css.css +++ b/wwwroot/css/calendar-components-css.css @@ -1,16 +1,5 @@ /* styles/components/navigation.css */ -/* Navigation bar */ -swp-calendar-nav { - display: flex; - align-items: center; - gap: 24px; - padding: 12px 16px; - background: var(--color-background); - border-bottom: 1px solid var(--color-border); - box-shadow: var(--shadow-sm); -} - /* Navigation groups */ swp-nav-group { display: flex; @@ -181,4 +170,23 @@ swp-calendar[data-searching="true"] { box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.3); } } +} + +/* Week info display */ +swp-week-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +swp-week-number { + font-size: 1.125rem; + font-weight: 600; + color: var(--color-text); +} + +swp-date-range { + font-size: 0.875rem; + color: var(--color-text-secondary); } \ No newline at end of file diff --git a/wwwroot/css/calendar-events-css.css b/wwwroot/css/calendar-events-css.css index 39bbe5f..b663a1a 100644 --- a/wwwroot/css/calendar-events-css.css +++ b/wwwroot/css/calendar-events-css.css @@ -6,12 +6,11 @@ swp-event { border-radius: 4px; overflow: hidden; cursor: move; - transition: box-shadow var(--transition-fast), transform var(--transition-fast); - z-index: var(--z-event); - - /* CSS-based positioning */ - top: calc(var(--start-minutes) * var(--minute-height)); - height: calc(var(--duration-minutes) * var(--minute-height)); + transition: box-shadow 150ms ease, transform 150ms ease; + z-index: 10; + left: 1px; + right: 1px; + padding: 8px; /* Event types */ &[data-type="meeting"] { @@ -34,110 +33,26 @@ swp-event { border-left: 4px solid var(--color-event-milestone-border); } - /* Hover state */ - &:hover { - box-shadow: var(--shadow-md); - transform: scale(1.02); - z-index: var(--z-event-hover); - - swp-resize-handle { - opacity: 1; - } - } - - /* Active/selected state */ - &[data-selected="true"] { - box-shadow: 0 0 0 2px var(--color-primary); - z-index: var(--z-event-hover); - } - - /* Dragging state */ - &[data-dragging="true"] { - opacity: 0.5; - cursor: grabbing; - z-index: var(--z-drag-ghost); - - &::before { - content: ''; - position: absolute; - inset: -2px; - border: 2px solid var(--color-primary); - border-radius: 6px; - pointer-events: none; - } - } - - /* Resizing state */ - &[data-resizing="true"] { - opacity: 0.8; - - swp-resize-handle { - opacity: 1; - - &::before, - &::after { - background: var(--color-primary); - } - } - } - - /* Sync status indicators */ - &[data-sync-status="pending"] { - &::after { - content: ''; - position: absolute; - top: 4px; - right: 4px; - width: 8px; - height: 8px; - background: var(--color-warning); - border-radius: 50%; - animation: pulse 2s infinite; - } - } - - &[data-sync-status="error"] { - &::after { - content: ''; - position: absolute; - top: 4px; - right: 4px; - width: 8px; - height: 8px; - background: var(--color-danger); - border-radius: 50%; - } - } } -/* Event header */ -swp-event-header { - padding: 8px 12px 4px; - - swp-event-time { - display: block; - font-size: 0.875rem; - font-weight: 500; - opacity: 0.8; - } +swp-event:hover { + box-shadow: var(--shadow-md); + transform: scale(1.02); + z-index: 20; } -/* Event body */ -swp-event-body { - padding: 0 12px 8px; - - swp-event-title { - display: block; - font-size: 0.875rem; - line-height: 1.3; - overflow: hidden; - text-overflow: ellipsis; - - /* Multi-line ellipsis */ - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - } +swp-event-time { + display: block; + font-size: 0.875rem; + font-weight: 500; + opacity: 0.8; + margin-bottom: 4px; +} + +swp-event-title { + display: block; + font-size: 0.875rem; + line-height: 1.3; } /* Resize handles */ diff --git a/wwwroot/css/calendar-layout-css.css b/wwwroot/css/calendar-layout-css.css index 74b4b9e..3d0a86a 100644 --- a/wwwroot/css/calendar-layout-css.css +++ b/wwwroot/css/calendar-layout-css.css @@ -9,24 +9,89 @@ swp-calendar { position: relative; } -/* Calendar container grid */ +/* Navigation bar layout */ +swp-calendar-nav { + display: grid; + grid-template-columns: auto 1fr auto auto; + align-items: center; + gap: 20px; + padding: 12px 16px; + background: var(--color-background); + border-bottom: 1px solid var(--color-border); + box-shadow: var(--shadow-sm); +} + +/* Calendar container grid (following POC structure) */ swp-calendar-container { flex: 1; display: grid; grid-template-columns: 60px 1fr; - grid-template-rows: auto 1fr; + grid-template-rows: 1fr; overflow: hidden; position: relative; } -/* Time axis (left side) */ +/* Time axis (fixed, left side) */ swp-time-axis { grid-column: 1; - grid-row: 2; + grid-row: 1; background: var(--color-surface); border-right: 1px solid var(--color-border); + position: sticky; + left: 0; + z-index: 4; + padding-top: 80px; /* Match header height */ + overflow-y: hidden; /* Hide scrollbar but allow programmatic scrolling */ + overflow-x: hidden; +} + +/* Week container for sliding */ +swp-week-container { + grid-column: 2; + display: grid; + grid-template-rows: auto 1fr; position: relative; - z-index: 2; + width: 100%; + transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Week header (inside week container) */ +swp-week-header { + display: grid; + grid-template-columns: repeat(7, 1fr); + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + position: sticky; + top: 0; + z-index: 3; + height: 80px; /* Fixed height */ +} + +/* Scrollable content */ +swp-scrollable-content { + overflow-y: auto; + overflow-x: hidden; + scroll-behavior: smooth; + position: relative; + flex: 1; + min-height: 0; /* Important for flex children to shrink */ + max-height: calc(100vh - 80px - 80px); /* Subtract nav height and week-header height */ +} + +swp-week-container.slide-out-left { + transform: translateX(-100%); +} + +swp-week-container.slide-out-right { + transform: translateX(100%); +} + +swp-week-container.slide-in-left { + transform: translateX(-100%); +} + +swp-week-container.slide-in-right { + transform: translateX(100%); } swp-hour-marker { @@ -51,66 +116,46 @@ swp-hour-marker { } } -/* Week header */ -swp-week-header { - grid-column: 2; - grid-row: 1; - display: grid; - grid-template-columns: repeat(var(--week-days, 7), 1fr); - background: var(--color-surface); - border-bottom: 1px solid var(--color-border); - position: sticky; - top: 0; - z-index: 3; -} +/* Day header styling (inside week-header) */ swp-day-header { padding: 12px; text-align: center; border-right: 1px solid var(--color-grid-line); - - &:last-child { - border-right: none; - } - - swp-day-name { - display: block; - font-weight: 500; - font-size: 0.875rem; - color: var(--color-text-secondary); - } - - swp-day-date { - display: block; - font-size: 1.25rem; - font-weight: 600; - margin-top: 4px; - } - - /* Today indicator */ - &[data-today="true"] { - swp-day-date { - color: var(--color-primary); - background: rgba(33, 150, 243, 0.1); - border-radius: 50%; - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - margin: 4px auto 0; - } - } + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } -/* Scrollable content */ -swp-scrollable-content { - grid-column: 2; - grid-row: 2; - overflow-y: auto; - overflow-x: hidden; - scroll-behavior: smooth; - position: relative; +swp-day-header:last-child { + border-right: none; +} + +swp-day-name { + display: block; + font-weight: 500; + font-size: 0.875rem; + color: var(--color-text-secondary); +} + +swp-day-date { + display: block; + font-size: 1.25rem; + font-weight: 600; + margin-top: 4px; +} + +swp-day-header[data-today="true"] swp-day-date { + color: var(--color-primary); + background: rgba(33, 150, 243, 0.1); + border-radius: 50%; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + margin: 4px auto 0; } /* All-day events container */ @@ -130,19 +175,18 @@ swp-allday-container { /* Time grid */ swp-time-grid { position: relative; - height: calc(var(--total-hours, 12) * var(--hour-height)); - - /* Work hours background */ - &::before { - content: ''; - position: absolute; - top: calc((var(--work-start-hour) - var(--day-start-hour)) * var(--hour-height)); - height: calc((var(--work-end-hour) - var(--work-start-hour)) * var(--hour-height)); - left: 0; - right: 0; - background: var(--color-work-hours); - pointer-events: none; - } + height: calc(24 * var(--hour-height)); +} + +swp-time-grid::before { + content: ''; + position: absolute; + top: calc((var(--work-start-hour) - var(--day-start-hour)) * var(--hour-height)); + height: calc((var(--work-end-hour) - var(--work-start-hour)) * var(--hour-height)); + left: 0; + right: 0; + background: var(--color-work-hours); + pointer-events: none; } /* Grid lines */ @@ -150,26 +194,39 @@ swp-grid-lines { position: absolute; inset: 0; pointer-events: none; - - /* 15-minute intervals */ - background-image: repeating-linear-gradient( - to bottom, - transparent, - transparent calc(var(--hour-height) / 4 - 1px), - var(--color-grid-line-light) calc(var(--hour-height) / 4 - 1px), - var(--color-grid-line-light) calc(var(--hour-height) / 4) - ); - - /* Show stronger lines when dragging */ - &[data-dragging="true"] { - background-image: repeating-linear-gradient( + z-index: var(--z-grid); + background-image: + /* Hour lines (stronger) */ + repeating-linear-gradient( + to bottom, + transparent, + transparent calc(var(--hour-height) - 1px), + var(--color-grid-line) calc(var(--hour-height) - 1px), + var(--color-grid-line) var(--hour-height) + ), + /* Quarter hour lines (lighter) */ + repeating-linear-gradient( to bottom, transparent, transparent calc(var(--hour-height) / 4 - 1px), - rgba(33, 150, 243, 0.2) calc(var(--hour-height) / 4 - 1px), - rgba(33, 150, 243, 0.2) calc(var(--hour-height) / 4) + var(--color-grid-line-light) calc(var(--hour-height) / 4 - 1px), + var(--color-grid-line-light) calc(var(--hour-height) / 4) ); - } +} + +/* Ensure grid lines are visible during transitions */ +swp-week-container swp-grid-lines { + opacity: 1; + visibility: visible; +} + +/* Grid lines should remain visible even during animations */ +swp-week-container.slide-out-left swp-grid-lines, +swp-week-container.slide-out-right swp-grid-lines, +swp-week-container.slide-in-left swp-grid-lines, +swp-week-container.slide-in-right swp-grid-lines { + opacity: 1; + visibility: visible; } /* Day columns */ @@ -177,42 +234,21 @@ swp-day-columns { position: absolute; inset: 0; display: grid; - grid-template-columns: repeat(var(--week-days, 7), 1fr); + grid-template-columns: repeat(7, 1fr); } swp-day-column { position: relative; border-right: 1px solid var(--color-grid-line); - - &:last-child { - border-right: none; - } - - /* Hover effect for empty slots */ - &:hover { - background: rgba(0, 0, 0, 0.01); - } } -/* Events layer */ +swp-day-column:last-child { + border-right: none; +} + swp-events-layer { position: absolute; inset: 0; - - /* Layout modes */ - &[data-layout="overlap"] { - swp-event { - width: calc(100% - 16px); - left: 8px; - } - } - - &[data-layout="side-by-side"] { - swp-event { - width: calc(var(--event-width, 100%) - 16px); - left: calc(8px + var(--event-offset, 0px)); - } - } } /* Current time indicator */ diff --git a/wwwroot/css/calendar-popup-css.css b/wwwroot/css/calendar-popup-css.css index 44484be..7a413fe 100644 --- a/wwwroot/css/calendar-popup-css.css +++ b/wwwroot/css/calendar-popup-css.css @@ -130,23 +130,27 @@ swp-loading-overlay { display: flex; align-items: center; justify-content: center; - z-index: var(--z-loading); - backdrop-filter: blur(2px); - - &[hidden] { - display: none; - } + z-index: 200; +} + +swp-loading-overlay[hidden] { + display: none; } swp-spinner { width: 40px; height: 40px; - border: 3px solid var(--color-surface); - border-top-color: var(--color-primary); + border: 3px solid #f3f3f3; + border-top: 3px solid var(--color-primary); border-radius: 50%; animation: spin 1s linear infinite; } +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + /* Snap indicator */ swp-snap-indicator { position: absolute; diff --git a/wwwroot/index.html b/wwwroot/index.html index 14d57f6..e467812 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -5,58 +5,12 @@ Calendar Plantempus - Week View - - - - - + + + + + +