Implements custom scroll and event logging

Adds custom scroll management for the calendar week view, replacing native scrollbars with a custom handle.

Introduces categorized event logging with console grouping and styling, enhancing debug output. It also allows configuring logging for specific event categories.
This commit is contained in:
Janus Knudsen 2025-07-29 00:52:01 +02:00
parent 001443ce11
commit 9f6d4333cb
7 changed files with 606 additions and 63 deletions

View file

@ -9,6 +9,17 @@ export class EventBus implements IEventBus {
private eventLog: EventLogEntry[] = []; private eventLog: EventLogEntry[] = [];
private debug: boolean = false; private debug: boolean = false;
private listeners: Set<ListenerEntry> = new Set(); private listeners: Set<ListenerEntry> = new Set();
// Log configuration for different categories
private logConfig: { [key: string]: boolean } = {
calendar: true,
grid: true,
event: true,
scroll: true,
navigation: true,
view: true,
default: true
};
/** /**
* Subscribe to an event via DOM addEventListener * Subscribe to an event via DOM addEventListener
@ -55,9 +66,9 @@ export class EventBus implements IEventBus {
cancelable: true cancelable: true
}); });
// Log event // Log event with grouping
if (this.debug) { if (this.debug) {
console.log(`📢 Event: ${eventType}`, detail); this.logEventWithGrouping(eventType, detail);
} }
this.eventLog.push({ this.eventLog.push({
@ -70,6 +81,77 @@ export class EventBus implements IEventBus {
return !document.dispatchEvent(event); return !document.dispatchEvent(event);
} }
/**
* Log event with console grouping
*/
private logEventWithGrouping(eventType: string, detail: any): void {
// Extract category from event type (e.g., 'calendar:datechanged' → 'calendar')
const category = this.extractCategory(eventType);
// Only log if category is enabled
if (!this.logConfig[category]) {
return;
}
// Get category emoji and color
const { emoji, color } = this.getCategoryStyle(category);
// Use collapsed group to reduce visual noise
console.groupCollapsed(`%c${emoji} ${category.toUpperCase()}`, `color: ${color}; font-weight: bold`);
console.log(`Event: ${eventType}`, detail);
console.groupEnd();
}
/**
* Extract category from event type
*/
private extractCategory(eventType: string): string {
if (eventType.includes(':')) {
return eventType.split(':')[0];
}
// Fallback: try to detect category from event name patterns
const lowerType = eventType.toLowerCase();
if (lowerType.includes('grid') || lowerType.includes('rendered')) return 'grid';
if (lowerType.includes('event') || lowerType.includes('sync')) return 'event';
if (lowerType.includes('scroll')) return 'scroll';
if (lowerType.includes('nav') || lowerType.includes('date')) return 'navigation';
if (lowerType.includes('view')) return 'view';
return 'default';
}
/**
* Get styling for different categories
*/
private getCategoryStyle(category: string): { emoji: string; color: string } {
const styles: { [key: string]: { emoji: string; color: string } } = {
calendar: { emoji: '🗓️', color: '#2196F3' },
grid: { emoji: '📊', color: '#4CAF50' },
event: { emoji: '📅', color: '#FF9800' },
scroll: { emoji: '📜', color: '#9C27B0' },
navigation: { emoji: '🧭', color: '#F44336' },
view: { emoji: '👁️', color: '#00BCD4' },
default: { emoji: '📢', color: '#607D8B' }
};
return styles[category] || styles.default;
}
/**
* Configure logging for specific categories
*/
setLogConfig(config: { [key: string]: boolean }): void {
this.logConfig = { ...this.logConfig, ...config };
}
/**
* Get current log configuration
*/
getLogConfig(): { [key: string]: boolean } {
return { ...this.logConfig };
}
/** /**
* Get event history * Get event history
*/ */

View file

@ -6,6 +6,7 @@ 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 { GridManager } from './managers/GridManager.js';
import { ScrollManager } from './managers/ScrollManager.js';
import { CalendarConfig } from './core/CalendarConfig.js'; import { CalendarConfig } from './core/CalendarConfig.js';
/** /**
@ -23,6 +24,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 scrollManager = new ScrollManager(); // Initialize BEFORE GridManager
const gridManager = new GridManager(); const gridManager = new GridManager();
// Enable debug mode for development // Enable debug mode for development
@ -41,7 +43,8 @@ function initializeCalendar(): void {
viewManager, viewManager,
eventManager, eventManager,
eventRenderer, eventRenderer,
gridManager gridManager,
scrollManager
}; };
} }

View file

@ -49,16 +49,10 @@ export class EventRenderer {
} }
private renderEvents(events: CalendarEvent[]): void { private renderEvents(events: CalendarEvent[]): void {
console.log(`EventRenderer: Rendering ${events.length} events`);
// Clear existing events first // Clear existing events first
this.clearEvents(); this.clearEvents();
// For now, just log events - proper rendering will be implemented later // For now, just emit event rendered - proper rendering will be implemented later
events.forEach(event => {
console.log(`EventRenderer: Event "${event.title}" from ${event.start} to ${event.end}`);
});
this.eventBus.emit(EventTypes.EVENT_RENDERED, { this.eventBus.emit(EventTypes.EVENT_RENDERED, {
count: events.length count: events.length
}); });

View file

@ -94,7 +94,9 @@ export class GridManager {
this.renderGrid(); this.renderGrid();
// Emit grid rendered event // Emit grid rendered event
console.log('GridManager: Emitting GRID_RENDERED event');
eventBus.emit(EventTypes.GRID_RENDERED); eventBus.emit(EventTypes.GRID_RENDERED);
console.log('GridManager: GRID_RENDERED event emitted');
} }
/** /**
@ -115,15 +117,48 @@ export class GridManager {
// Clear existing grid and rebuild POC structure // Clear existing grid and rebuild POC structure
this.grid.innerHTML = ''; this.grid.innerHTML = '';
// Create POC structure: time-axis + week-container // Create POC structure: header-spacer + time-axis + week-container + right-side
this.createHeaderSpacer();
this.createRightHeaderSpacer();
this.createTimeAxis(); this.createTimeAxis();
this.createRightColumn();
this.createWeekContainer(); this.createWeekContainer();
console.log('GridManager: Grid rendered successfully with POC structure'); console.log('GridManager: Grid rendered successfully with POC structure');
} }
/** /**
* Create time axis (left column) like in POC * Create header spacer to align time axis with week content
*/
private createHeaderSpacer(): void {
if (!this.grid) return;
const headerSpacer = document.createElement('swp-header-spacer');
this.grid.appendChild(headerSpacer);
}
/**
* Create right header spacer to align right column with week content
*/
private createRightHeaderSpacer(): void {
if (!this.grid) return;
const rightHeaderSpacer = document.createElement('swp-right-header-spacer');
this.grid.appendChild(rightHeaderSpacer);
}
/**
* Create right column beside week container
*/
private createRightColumn(): void {
if (!this.grid) return;
const rightColumn = document.createElement('swp-right-column');
this.grid.appendChild(rightColumn);
}
/**
* Create time axis (positioned beside week container) like in POC
*/ */
private createTimeAxis(): void { private createTimeAxis(): void {
if (!this.grid) return; if (!this.grid) return;
@ -132,7 +167,7 @@ export class GridManager {
const startHour = calendarConfig.get('dayStartHour'); const startHour = calendarConfig.get('dayStartHour');
const endHour = calendarConfig.get('dayEndHour'); const endHour = calendarConfig.get('dayEndHour');
for (let hour = startHour; hour <= endHour; hour++) { for (let hour = startHour; hour < endHour; hour++) {
const marker = document.createElement('swp-hour-marker'); const marker = document.createElement('swp-hour-marker');
const period = hour >= 12 ? 'PM' : 'AM'; const period = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour); const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);
@ -221,7 +256,6 @@ export class GridManager {
const column = document.createElement('swp-day-column'); const column = document.createElement('swp-day-column');
(column as any).dataset.date = this.formatDate(date); (column as any).dataset.date = this.formatDate(date);
console.log(`GridManager: Creating day column ${dayIndex} for date ${this.formatDate(date)}`);
// Add dummy content to force column width (temporary test) // Add dummy content to force column width (temporary test)
const dummyContent = document.createElement('div'); const dummyContent = document.createElement('div');

View file

@ -0,0 +1,356 @@
// Custom scroll management for calendar week container
import { eventBus } from '../core/EventBus';
import { calendarConfig } from '../core/CalendarConfig';
import { EventTypes } from '../constants/EventTypes';
/**
* Manages custom scrolling functionality for the calendar
*/
export class ScrollManager {
private rightColumn: HTMLElement | null = null;
private scrollHandle: HTMLElement | null = null;
private scrollableContent: HTMLElement | null = null;
private calendarContainer: HTMLElement | null = null;
private timeAxis: HTMLElement | null = null;
private resizeObserver: ResizeObserver | null = null;
private isDragging: boolean = false;
private dragStartY: number = 0;
private scrollStartTop: number = 0;
private maxScrollTop: number = 0;
private handleHeight: number = 40;
constructor() {
this.init();
}
private init(): void {
this.subscribeToEvents();
}
private subscribeToEvents(): void {
// Initialize scroll when grid is rendered
eventBus.on(EventTypes.GRID_RENDERED, () => {
console.log('ScrollManager: Received GRID_RENDERED event');
this.setupScrolling();
});
// Handle mouse events for dragging
document.addEventListener('mousemove', this.handleMouseMove.bind(this));
document.addEventListener('mouseup', this.handleMouseUp.bind(this));
// Handle window resize
window.addEventListener('resize', () => {
this.updateScrollableHeight();
});
}
/**
* Setup scrolling functionality after grid is rendered
*/
private setupScrolling(): void {
this.findElements();
if (this.rightColumn && this.scrollableContent && this.calendarContainer) {
this.setupResizeObserver();
this.updateScrollableHeight();
this.createScrollHandle();
this.hideNativeScrollbar();
this.setupScrollSynchronization();
this.calculateScrollBounds();
this.updateHandlePosition();
}
}
/**
* Find DOM elements needed for scrolling
*/
private findElements(): void {
this.rightColumn = document.querySelector('swp-right-column');
this.scrollableContent = document.querySelector('swp-scrollable-content');
this.calendarContainer = document.querySelector('swp-calendar-container');
this.timeAxis = document.querySelector('swp-time-axis');
}
/**
* Create and add scroll handle to right column
*/
private createScrollHandle(): void {
if (!this.rightColumn) return;
// Remove existing handle if any
const existingHandle = this.rightColumn.querySelector('swp-scroll-handle');
if (existingHandle) {
existingHandle.remove();
}
// Create new handle
this.scrollHandle = document.createElement('swp-scroll-handle');
this.scrollHandle.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.rightColumn.appendChild(this.scrollHandle);
}
/**
* Calculate scroll bounds based on content and container heights
*/
private calculateScrollBounds(): void {
if (!this.scrollableContent || !this.rightColumn) return;
const contentHeight = this.scrollableContent.scrollHeight;
const containerHeight = this.scrollableContent.clientHeight;
// Use container height as track height since right column should match scrollable area
const trackHeight = containerHeight;
console.log('ScrollManager Debug:');
console.log('- contentHeight (scrollHeight):', contentHeight);
console.log('- containerHeight (clientHeight):', containerHeight);
console.log('- trackHeight (using containerHeight):', trackHeight);
console.log('- scrollableContent element:', this.scrollableContent);
this.maxScrollTop = Math.max(0, contentHeight - containerHeight);
// Calculate proportional handle height based on content ratio
if (contentHeight > 0 && containerHeight > 0) {
const visibleRatio = containerHeight / contentHeight;
this.handleHeight = Math.max(20, Math.min(trackHeight * visibleRatio, trackHeight - 10));
} else {
this.handleHeight = 40; // fallback
}
console.log('- maxScrollTop:', this.maxScrollTop);
console.log('- visibleRatio:', (containerHeight / contentHeight).toFixed(3));
console.log('- calculated handleHeight:', this.handleHeight);
// Update handle height in DOM
if (this.scrollHandle) {
this.scrollHandle.style.height = `${this.handleHeight}px`;
}
}
/**
* Handle mouse down on scroll handle
*/
private handleMouseDown(e: MouseEvent): void {
e.preventDefault();
this.isDragging = true;
this.dragStartY = e.clientY;
if (this.scrollHandle && this.scrollableContent) {
this.scrollHandle.classList.add('dragging');
this.scrollStartTop = this.scrollableContent.scrollTop;
}
}
/**
* Handle mouse move during drag
*/
private handleMouseMove(e: MouseEvent): void {
if (!this.isDragging || !this.scrollHandle || !this.scrollableContent) return;
e.preventDefault();
const deltaY = e.clientY - this.dragStartY;
// Use container height as track height
const trackHeight = this.scrollableContent.clientHeight - this.handleHeight;
// Ensure trackHeight is positive to avoid division by zero
if (trackHeight <= 0) return;
const scrollRatio = deltaY / trackHeight;
const newScrollTop = this.scrollStartTop + (scrollRatio * this.maxScrollTop);
// Clamp scroll position
const clampedScrollTop = Math.max(0, Math.min(newScrollTop, this.maxScrollTop));
// Apply scroll to content
this.scrollableContent.scrollTop = clampedScrollTop;
// Update handle position (this will also trigger time-axis sync via scroll event)
this.updateHandlePosition();
}
/**
* Handle mouse up to end drag
*/
private handleMouseUp(e: MouseEvent): void {
if (!this.isDragging) return;
this.isDragging = false;
if (this.scrollHandle) {
this.scrollHandle.classList.remove('dragging');
}
}
/**
* Update handle position based on current scroll
*/
private updateHandlePosition(): void {
if (!this.scrollHandle || !this.scrollableContent) return;
const scrollTop = this.scrollableContent.scrollTop;
const scrollRatio = this.maxScrollTop > 0 ? scrollTop / this.maxScrollTop : 0;
// Use container height as track height
const trackHeight = this.scrollableContent.clientHeight - this.handleHeight;
const handleTop = Math.max(0, Math.min(scrollRatio * trackHeight, trackHeight));
this.scrollHandle.style.top = `${handleTop}px`;
// Debug logging for handle position
if (scrollTop % 200 === 0) { // Log every 200px to avoid spam
console.log(`ScrollManager: Handle position - scrollTop: ${scrollTop}, ratio: ${scrollRatio.toFixed(3)}, handleTop: ${handleTop.toFixed(1)}, trackHeight: ${trackHeight}`);
}
}
/**
* Scroll to specific position
*/
scrollTo(scrollTop: number): void {
if (!this.scrollableContent) return;
const clampedScrollTop = Math.max(0, Math.min(scrollTop, this.maxScrollTop));
this.scrollableContent.scrollTop = clampedScrollTop;
this.updateHandlePosition();
}
/**
* Scroll to specific hour
*/
scrollToHour(hour: number): void {
const hourHeight = calendarConfig.get('hourHeight');
const dayStartHour = calendarConfig.get('dayStartHour');
const scrollTop = (hour - dayStartHour) * hourHeight;
this.scrollTo(scrollTop);
}
/**
* Setup ResizeObserver to monitor container size changes
*/
private setupResizeObserver(): void {
if (!this.calendarContainer) return;
// Clean up existing observer
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log('ScrollManager: Container resized', entry.contentRect);
this.updateScrollableHeight();
}
});
this.resizeObserver.observe(this.calendarContainer);
}
/**
* Calculate and update scrollable content height dynamically
*/
private updateScrollableHeight(): void {
if (!this.scrollableContent || !this.calendarContainer) return;
// Get calendar container height
const containerRect = this.calendarContainer.getBoundingClientRect();
// Find navigation height
const navigation = document.querySelector('swp-calendar-nav');
const navHeight = navigation ? navigation.getBoundingClientRect().height : 0;
// Find week header height
const weekHeader = document.querySelector('swp-week-header');
const headerHeight = weekHeader ? weekHeader.getBoundingClientRect().height : 80;
// Calculate available height for scrollable content
const availableHeight = containerRect.height - headerHeight;
console.log('ScrollManager: Dynamic height calculation');
console.log('- Container height:', containerRect.height);
console.log('- Navigation height:', navHeight);
console.log('- Header height:', headerHeight);
console.log('- Available height:', availableHeight);
// Set the height on scrollable content
if (availableHeight > 0) {
this.scrollableContent.style.height = `${availableHeight}px`;
// Recalculate scroll bounds after height change
setTimeout(() => {
this.calculateScrollBounds();
this.updateHandlePosition();
}, 0);
}
}
/**
* Hide native scrollbar while keeping scroll functionality
*/
private hideNativeScrollbar(): void {
if (!this.scrollableContent) return;
// Apply CSS to hide scrollbar
this.scrollableContent.style.scrollbarWidth = 'none'; // Firefox
(this.scrollableContent.style as any).msOverflowStyle = 'none'; // IE/Edge
// Add webkit scrollbar hiding
const style = document.createElement('style');
style.textContent = `
swp-scrollable-content::-webkit-scrollbar {
display: none; /* Chrome/Safari/Opera */
}
`;
document.head.appendChild(style);
}
/**
* Setup scroll synchronization between scrollable content and time axis
*/
private setupScrollSynchronization(): void {
if (!this.scrollableContent || !this.timeAxis) return;
console.log('ScrollManager: Setting up scroll synchronization');
// Throttle scroll events for better performance
let scrollTimeout: number | null = null;
this.scrollableContent.addEventListener('scroll', () => {
if (scrollTimeout) {
cancelAnimationFrame(scrollTimeout);
}
scrollTimeout = requestAnimationFrame(() => {
this.syncTimeAxisPosition();
this.updateHandlePosition();
});
});
}
/**
* Synchronize time axis position with scrollable content
*/
private syncTimeAxisPosition(): void {
if (!this.scrollableContent || !this.timeAxis) return;
const scrollTop = this.scrollableContent.scrollTop;
// Use transform for smooth performance
this.timeAxis.style.transform = `translateY(-${scrollTop}px)`;
// Debug logging (can be removed later)
if (scrollTop % 100 === 0) { // Only log every 100px to avoid spam
console.log(`ScrollManager: Synced time-axis to scrollTop: ${scrollTop}px`);
}
}
/**
* Cleanup resources
*/
destroy(): void {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
}
}

View file

@ -1,12 +1,26 @@
/* styles/layout.css - POC Structure Implementation */ /* styles/layout.css - POC Structure Implementation */
/* Main calendar container */ /* Calendar wrapper container - full viewport */
.calendar-wrapper {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
}
/* Main calendar container - full height */
swp-calendar { swp-calendar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
width: 100%;
background: var(--color-background); background: var(--color-background);
position: relative; position: relative;
overflow: hidden;
} }
/* Navigation bar layout */ /* Navigation bar layout */
@ -25,16 +39,37 @@ swp-calendar-nav {
swp-calendar-container { swp-calendar-container {
flex: 1; flex: 1;
display: grid; display: grid;
grid-template-columns: 60px 1fr; grid-template-columns: 60px 1fr 20px;
grid-template-rows: 1fr; grid-template-rows: auto 1fr;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
/* Header spacer for time axis alignment */
swp-header-spacer {
grid-column: 1;
grid-row: 1;
height: 80px; /* Same as week header height */
background: var(--color-surface);
border-right: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
/* Right header spacer */
swp-right-header-spacer {
grid-column: 3;
grid-row: 1;
height: 80px; /* Same as week header height */
background: var(--color-surface);
border-left: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
/* Week container for sliding */ /* Week container for sliding */
swp-week-container { swp-week-container {
grid-column: 2; grid-column: 2;
grid-row: 1 / 3;
display: grid; display: grid;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
position: relative; position: relative;
@ -45,13 +80,49 @@ swp-week-container {
/* Time axis */ /* Time axis */
swp-time-axis { swp-time-axis {
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 2;
background: var(--color-surface); background: var(--color-surface);
border-right: 1px solid var(--color-border); border-right: 1px solid var(--color-border);
position: sticky; position: sticky;
left: 0; left: 0;
z-index: 4; z-index: 4;
padding-top: 80px; /* Match header height */ width: 60px;
display: flex;
flex-direction: column;
}
/* Right column */
swp-right-column {
grid-column: 3;
grid-row: 2;
background: #f0f0f0;
border-left: 2px solid #333;
position: relative;
z-index: 4;
width: 20px;
overflow: hidden;
}
/* Scroll handle */
swp-scroll-handle {
position: absolute;
top: 0;
left: 2px;
width: 16px;
height: 40px;
background: #666;
border-radius: 8px;
cursor: grab;
transition: background-color 0.2s ease;
z-index: 5;
}
swp-scroll-handle:hover {
background: #333;
}
swp-scroll-handle.dragging {
background: #007bff;
} }
swp-hour-marker { swp-hour-marker {
@ -134,12 +205,13 @@ swp-scrollable-content {
scroll-behavior: smooth; scroll-behavior: smooth;
position: relative; position: relative;
display: grid; display: grid;
/* Height will be set dynamically by ScrollManager via ResizeObserver */
} }
/* Time grid */ /* Time grid */
swp-time-grid { swp-time-grid {
position: relative; position: relative;
height: calc(12 * var(--hour-height)); height: calc((var(--day-end-hour) - var(--day-start-hour)) * var(--hour-height));
} }
swp-time-grid::before { swp-time-grid::before {

View file

@ -13,51 +13,53 @@
<link rel="stylesheet" href="css/calendar-popup-css.css"> <link rel="stylesheet" href="css/calendar-popup-css.css">
</head> </head>
<body> <body>
<swp-calendar data-view="week" data-week-days="7" data-snap-interval="15"> <div class="calendar-wrapper">
<!-- Navigation Bar --> <swp-calendar data-view="week" data-week-days="7" data-snap-interval="15">
<swp-calendar-nav> <!-- Navigation Bar -->
<swp-nav-group> <swp-calendar-nav>
<swp-nav-button data-action="prev"></swp-nav-button> <swp-nav-group>
<swp-nav-button data-action="next"></swp-nav-button> <swp-nav-button data-action="prev"></swp-nav-button>
<swp-nav-button data-action="today">Today</swp-nav-button> <swp-nav-button data-action="next"></swp-nav-button>
</swp-nav-group> <swp-nav-button data-action="today">Today</swp-nav-button>
</swp-nav-group>
<swp-week-info>
<swp-week-number>Week 3</swp-week-number>
<swp-date-range>Jan 15 - Jan 21, 2024</swp-date-range>
</swp-week-info>
<swp-search-container>
<swp-search-icon>
<svg width="16" height="16" viewBox="0 0 16 16">
<circle cx="6" cy="6" r="5" stroke="currentColor" fill="none" stroke-width="1.5"/>
<path d="M10 10l4 4" stroke="currentColor" stroke-width="1.5"/>
</svg>
</swp-search-icon>
<input type="search" placeholder="Search events..." />
<swp-search-clear hidden>
<svg width="14" height="14" viewBox="0 0 16 16">
<path d="M5 5l6 6M11 5l-6 6" stroke="currentColor" stroke-width="2"/>
</svg>
</swp-search-clear>
</swp-search-container>
<swp-view-selector>
<swp-view-button data-view="day">Day</swp-view-button>
<swp-view-button data-view="week" data-active="true">Week</swp-view-button>
<swp-view-button data-view="month" disabled>Month</swp-view-button>
</swp-view-selector>
</swp-calendar-nav>
<swp-week-info> <!-- Calendar Grid Container -->
<swp-week-number>Week 3</swp-week-number> <swp-calendar-container>
<swp-date-range>Jan 15 - Jan 21, 2024</swp-date-range> <!-- Headers and time slots will be generated dynamically by JavaScript -->
</swp-week-info> </swp-calendar-container>
<swp-search-container> <swp-loading-overlay hidden>
<swp-search-icon> <swp-spinner></swp-spinner>
<svg width="16" height="16" viewBox="0 0 16 16"> </swp-loading-overlay>
<circle cx="6" cy="6" r="5" stroke="currentColor" fill="none" stroke-width="1.5"/> </swp-calendar>
<path d="M10 10l4 4" stroke="currentColor" stroke-width="1.5"/> </div>
</svg>
</swp-search-icon>
<input type="search" placeholder="Search events..." />
<swp-search-clear hidden>
<svg width="14" height="14" viewBox="0 0 16 16">
<path d="M5 5l6 6M11 5l-6 6" stroke="currentColor" stroke-width="2"/>
</svg>
</swp-search-clear>
</swp-search-container>
<swp-view-selector>
<swp-view-button data-view="day">Day</swp-view-button>
<swp-view-button data-view="week" data-active="true">Week</swp-view-button>
<swp-view-button data-view="month" disabled>Month</swp-view-button>
</swp-view-selector>
</swp-calendar-nav>
<!-- Calendar Grid Container -->
<swp-calendar-container>
<!-- Headers and time slots will be generated dynamically by JavaScript -->
</swp-calendar-container>
<swp-loading-overlay hidden>
<swp-spinner></swp-spinner>
</swp-loading-overlay>
</swp-calendar>
<!-- JavaScript Bundle --> <!-- JavaScript Bundle -->
<script type="module" src="js/calendar.js"></script> <script type="module" src="js/calendar.js"></script>