Improves grid layout and navigation

Refactors the calendar grid to support smoother navigation transitions by using separate week containers.

This change introduces a GridManager to handle grid rendering and interactions within these containers, enabling scroll synchronization and click event handling for each week view.

Also, configures the calendar to start at midnight and span a full 24-hour day, providing a more comprehensive view.
This commit is contained in:
Janus Knudsen 2025-07-25 00:24:15 +02:00
parent f06c02121c
commit b443649ced
12 changed files with 719 additions and 302 deletions

24
Calendar Plantempus.sln Normal file
View file

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

View file

@ -27,8 +27,8 @@ export class CalendarConfig {
firstDayOfWeek: 1, // 0 = Sunday, 1 = Monday firstDayOfWeek: 1, // 0 = Sunday, 1 = Monday
// Time settings // Time settings
dayStartHour: 7, // Calendar starts at 7 AM dayStartHour: 0, // Calendar starts at midnight
dayEndHour: 19, // Calendar ends at 7 PM dayEndHour: 24, // Calendar ends at midnight (24 hours)
workStartHour: 8, // Work hours start workStartHour: 8, // Work hours start
workEndHour: 17, // Work hours end workEndHour: 17, // Work hours end
snapInterval: 15, // Minutes: 5, 10, 15, 30, 60 snapInterval: 15, // Minutes: 5, 10, 15, 30, 60

View file

@ -5,6 +5,7 @@ import { NavigationManager } from './managers/NavigationManager.js';
import { ViewManager } from './managers/ViewManager.js'; import { ViewManager } from './managers/ViewManager.js';
import { EventManager } from './managers/EventManager.js'; import { EventManager } from './managers/EventManager.js';
import { EventRenderer } from './managers/EventRenderer.js'; import { EventRenderer } from './managers/EventRenderer.js';
import { GridManager } from './managers/GridManager.js';
import { CalendarConfig } from './core/CalendarConfig.js'; import { CalendarConfig } from './core/CalendarConfig.js';
/** /**
@ -22,6 +23,7 @@ function initializeCalendar(): void {
const viewManager = new ViewManager(eventBus); const viewManager = new ViewManager(eventBus);
const eventManager = new EventManager(eventBus); const eventManager = new EventManager(eventBus);
const eventRenderer = new EventRenderer(eventBus); const eventRenderer = new EventRenderer(eventBus);
const gridManager = new GridManager();
// Enable debug mode for development // Enable debug mode for development
eventBus.setDebug(true); eventBus.setDebug(true);
@ -38,7 +40,8 @@ function initializeCalendar(): void {
navigationManager, navigationManager,
viewManager, viewManager,
eventManager, eventManager,
eventRenderer eventRenderer,
gridManager
}; };
} }

View file

@ -33,6 +33,7 @@ export class GridManager {
private init(): void { private init(): void {
this.findElements(); this.findElements();
this.subscribeToEvents(); this.subscribeToEvents();
this.setupScrollSync();
} }
private findElements(): void { private findElements(): void {
@ -64,6 +65,19 @@ export class GridManager {
this.renderHeaders(); 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 // Handle grid clicks
this.setupGridInteractions(); this.setupGridInteractions();
} }
@ -345,4 +359,228 @@ export class GridManager {
this.scrollableContent.scrollTop = scrollTop; 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 = `
<swp-day-name>${days[date.getDay()]}</swp-day-name>
<swp-day-date>${date.getDate()}</swp-day-date>
`;
(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;
}
});
}
} }

View file

@ -105,15 +105,15 @@ export class NavigationManager {
} }
private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void { 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'); const currentWeekContainer = document.querySelector('swp-week-container');
if (!container || !currentWeekContainer) { if (!calendarContainer || !currentWeekContainer) {
console.warn('NavigationManager: Required DOM elements not found'); console.warn('NavigationManager: Required DOM elements not found');
return; return;
} }
// Create new week container // Create new week container (following POC structure)
const newWeekContainer = document.createElement('swp-week-container'); const newWeekContainer = document.createElement('swp-week-container');
newWeekContainer.innerHTML = ` newWeekContainer.innerHTML = `
<swp-week-header></swp-week-header> <swp-week-header></swp-week-header>
@ -133,8 +133,8 @@ export class NavigationManager {
newWeekContainer.style.height = '100%'; newWeekContainer.style.height = '100%';
newWeekContainer.style.transform = direction === 'next' ? 'translateX(100%)' : 'translateX(-100%)'; newWeekContainer.style.transform = direction === 'next' ? 'translateX(100%)' : 'translateX(-100%)';
// Add to container // Add to calendar container
container.appendChild(newWeekContainer); calendarContainer.appendChild(newWeekContainer);
// Notify other managers to render content for the new week // Notify other managers to render content for the new week
this.eventBus.emit(EventTypes.WEEK_CONTAINER_CREATED, { this.eventBus.emit(EventTypes.WEEK_CONTAINER_CREATED, {

218
src/utils/PositionUtils.js Normal file
View file

@ -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);
}
}

View file

@ -15,8 +15,8 @@
--snap-interval: 15; --snap-interval: 15;
/* Time boundaries */ /* Time boundaries */
--day-start-hour: 7; --day-start-hour: 0;
--day-end-hour: 19; --day-end-hour: 24;
--work-start-hour: 8; --work-start-hour: 8;
--work-end-hour: 17; --work-end-hour: 17;
@ -94,9 +94,26 @@ swp-day-columns,
swp-day-column, swp-day-column,
swp-events-layer, swp-events-layer,
swp-event, swp-event,
swp-allday-container,
swp-loading-overlay, 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; display: block;
} }

View file

@ -1,16 +1,5 @@
/* styles/components/navigation.css */ /* 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 */ /* Navigation groups */
swp-nav-group { swp-nav-group {
display: flex; display: flex;
@ -182,3 +171,22 @@ swp-calendar[data-searching="true"] {
} }
} }
} }
/* 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);
}

View file

@ -6,12 +6,11 @@ swp-event {
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
cursor: move; cursor: move;
transition: box-shadow var(--transition-fast), transform var(--transition-fast); transition: box-shadow 150ms ease, transform 150ms ease;
z-index: var(--z-event); z-index: 10;
left: 1px;
/* CSS-based positioning */ right: 1px;
top: calc(var(--start-minutes) * var(--minute-height)); padding: 8px;
height: calc(var(--duration-minutes) * var(--minute-height));
/* Event types */ /* Event types */
&[data-type="meeting"] { &[data-type="meeting"] {
@ -34,110 +33,26 @@ swp-event {
border-left: 4px solid var(--color-event-milestone-border); 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:hover {
swp-event-header { box-shadow: var(--shadow-md);
padding: 8px 12px 4px; transform: scale(1.02);
z-index: 20;
swp-event-time {
display: block;
font-size: 0.875rem;
font-weight: 500;
opacity: 0.8;
}
} }
/* Event body */ swp-event-time {
swp-event-body { display: block;
padding: 0 12px 8px; font-size: 0.875rem;
font-weight: 500;
opacity: 0.8;
margin-bottom: 4px;
}
swp-event-title { swp-event-title {
display: block; display: block;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.3; line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
/* Multi-line ellipsis */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
} }
/* Resize handles */ /* Resize handles */

View file

@ -9,24 +9,89 @@ swp-calendar {
position: relative; 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 { swp-calendar-container {
flex: 1; flex: 1;
display: grid; display: grid;
grid-template-columns: 60px 1fr; grid-template-columns: 60px 1fr;
grid-template-rows: auto 1fr; grid-template-rows: 1fr;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
/* Time axis (left side) */ /* Time axis (fixed, left side) */
swp-time-axis { swp-time-axis {
grid-column: 1; grid-column: 1;
grid-row: 2; grid-row: 1;
background: var(--color-surface); background: var(--color-surface);
border-right: 1px solid var(--color-border); 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; 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 { swp-hour-marker {
@ -51,66 +116,46 @@ swp-hour-marker {
} }
} }
/* Week header */ /* Day header styling (inside 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;
}
swp-day-header { swp-day-header {
padding: 12px; padding: 12px;
text-align: center; text-align: center;
border-right: 1px solid var(--color-grid-line); border-right: 1px solid var(--color-grid-line);
display: flex;
&:last-child { flex-direction: column;
border-right: none; align-items: center;
} justify-content: center;
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;
}
}
} }
/* Scrollable content */ swp-day-header:last-child {
swp-scrollable-content { border-right: none;
grid-column: 2; }
grid-row: 2;
overflow-y: auto; swp-day-name {
overflow-x: hidden; display: block;
scroll-behavior: smooth; font-weight: 500;
position: relative; 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 */ /* All-day events container */
@ -130,19 +175,18 @@ swp-allday-container {
/* Time grid */ /* Time grid */
swp-time-grid { swp-time-grid {
position: relative; position: relative;
height: calc(var(--total-hours, 12) * var(--hour-height)); height: calc(24 * var(--hour-height));
}
/* Work hours background */ swp-time-grid::before {
&::before { content: '';
content: ''; position: absolute;
position: absolute; top: calc((var(--work-start-hour) - var(--day-start-hour)) * var(--hour-height));
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));
height: calc((var(--work-end-hour) - var(--work-start-hour)) * var(--hour-height)); left: 0;
left: 0; right: 0;
right: 0; background: var(--color-work-hours);
background: var(--color-work-hours); pointer-events: none;
pointer-events: none;
}
} }
/* Grid lines */ /* Grid lines */
@ -150,26 +194,39 @@ swp-grid-lines {
position: absolute; position: absolute;
inset: 0; inset: 0;
pointer-events: none; pointer-events: none;
z-index: var(--z-grid);
/* 15-minute intervals */ background-image:
background-image: repeating-linear-gradient( /* Hour lines (stronger) */
to bottom, repeating-linear-gradient(
transparent, to bottom,
transparent calc(var(--hour-height) / 4 - 1px), transparent,
var(--color-grid-line-light) calc(var(--hour-height) / 4 - 1px), transparent calc(var(--hour-height) - 1px),
var(--color-grid-line-light) calc(var(--hour-height) / 4) var(--color-grid-line) calc(var(--hour-height) - 1px),
); var(--color-grid-line) var(--hour-height)
),
/* Show stronger lines when dragging */ /* Quarter hour lines (lighter) */
&[data-dragging="true"] { repeating-linear-gradient(
background-image: repeating-linear-gradient(
to bottom, to bottom,
transparent, transparent,
transparent calc(var(--hour-height) / 4 - 1px), transparent calc(var(--hour-height) / 4 - 1px),
rgba(33, 150, 243, 0.2) calc(var(--hour-height) / 4 - 1px), var(--color-grid-line-light) 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)
); );
} }
/* 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 */ /* Day columns */
@ -177,42 +234,21 @@ swp-day-columns {
position: absolute; position: absolute;
inset: 0; inset: 0;
display: grid; display: grid;
grid-template-columns: repeat(var(--week-days, 7), 1fr); grid-template-columns: repeat(7, 1fr);
} }
swp-day-column { swp-day-column {
position: relative; position: relative;
border-right: 1px solid var(--color-grid-line); 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 { swp-events-layer {
position: absolute; position: absolute;
inset: 0; 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 */ /* Current time indicator */

View file

@ -130,23 +130,27 @@ swp-loading-overlay {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: var(--z-loading); z-index: 200;
backdrop-filter: blur(2px); }
&[hidden] { swp-loading-overlay[hidden] {
display: none; display: none;
}
} }
swp-spinner { swp-spinner {
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 3px solid var(--color-surface); border: 3px solid #f3f3f3;
border-top-color: var(--color-primary); border-top: 3px solid var(--color-primary);
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Snap indicator */ /* Snap indicator */
swp-snap-indicator { swp-snap-indicator {
position: absolute; position: absolute;

View file

@ -5,58 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar Plantempus - Week View</title> <title>Calendar Plantempus - Week View</title>
<!-- CSS Files --> <!-- Modular CSS Files -->
<link rel="stylesheet" href="css/calendar.css"> <link rel="stylesheet" href="css/calendar-base-css.css">
<link rel="stylesheet" href="css/calendar-layout-css.css">
<!-- Additional styles for view selector --> <link rel="stylesheet" href="css/calendar-components-css.css">
<style> <link rel="stylesheet" href="css/calendar-events-css.css">
swp-view-selector { <link rel="stylesheet" href="css/calendar-popup-css.css">
display: flex;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 4px;
overflow: hidden;
}
swp-view-button {
padding: 8px 16px;
border: none;
background: transparent;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 150ms ease;
}
swp-view-button:not(:last-child) {
border-right: 1px solid var(--color-border);
}
swp-view-button[data-active="true"] {
background: var(--color-primary);
color: white;
}
/* Week info display */
swp-week-info {
display: flex;
align-items: center;
gap: 12px;
font-size: 0.875rem;
color: var(--color-text-secondary);
justify-self: start;
padding-left: 20px;
}
swp-week-number {
font-weight: 600;
color: var(--color-text);
}
swp-date-range {
color: var(--color-text-secondary);
}
</style>
</head> </head>
<body> <body>
<swp-calendar data-view="week" data-week-days="7" data-snap-interval="15"> <swp-calendar data-view="week" data-week-days="7" data-snap-interval="15">