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
-
-
-
-
-
+
+
+
+
+
+