Cleanup test files and move to another folder
This commit is contained in:
parent
8456d8aa28
commit
9c765b35ab
28 changed files with 0 additions and 1981 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
189
.workbench/poc-layouts/calendar-config.js
Normal file
189
.workbench/poc-layouts/calendar-config.js
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
// js/core/CalendarConfig.js
|
||||
|
||||
import { eventBus } from './EventBus.js';
|
||||
import { EventTypes } from '../types/EventTypes.js';
|
||||
|
||||
/**
|
||||
* Calendar configuration management
|
||||
*/
|
||||
export class CalendarConfig {
|
||||
constructor() {
|
||||
this.config = {
|
||||
// View settings
|
||||
view: 'week', // 'day' | 'week' | 'month'
|
||||
weekDays: 7, // 4-7 days for week view
|
||||
firstDayOfWeek: 1, // 0 = Sunday, 1 = Monday
|
||||
|
||||
// Time settings
|
||||
dayStartHour: 7, // Calendar starts at 7 AM
|
||||
dayEndHour: 19, // Calendar ends at 7 PM
|
||||
workStartHour: 8, // Work hours start
|
||||
workEndHour: 17, // Work hours end
|
||||
snapInterval: 15, // Minutes: 5, 10, 15, 30, 60
|
||||
|
||||
// Display settings
|
||||
hourHeight: 60, // Pixels per hour
|
||||
showCurrentTime: true,
|
||||
showWorkHours: true,
|
||||
|
||||
// Interaction settings
|
||||
allowDrag: true,
|
||||
allowResize: true,
|
||||
allowCreate: true,
|
||||
|
||||
// API settings
|
||||
apiEndpoint: '/api/events',
|
||||
dateFormat: 'YYYY-MM-DD',
|
||||
timeFormat: 'HH:mm',
|
||||
|
||||
// Feature flags
|
||||
enableSearch: true,
|
||||
enableTouch: true,
|
||||
|
||||
// Event defaults
|
||||
defaultEventDuration: 60, // Minutes
|
||||
minEventDuration: null, // Will be same as snapInterval
|
||||
maxEventDuration: 480 // 8 hours
|
||||
};
|
||||
|
||||
// Set computed values
|
||||
this.config.minEventDuration = this.config.snapInterval;
|
||||
|
||||
// Load from data attributes
|
||||
this.loadFromDOM();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from DOM data attributes
|
||||
*/
|
||||
loadFromDOM() {
|
||||
const calendar = document.querySelector('swp-calendar');
|
||||
if (!calendar) return;
|
||||
|
||||
// Read data attributes
|
||||
const attrs = calendar.dataset;
|
||||
|
||||
if (attrs.view) this.config.view = attrs.view;
|
||||
if (attrs.weekDays) this.config.weekDays = parseInt(attrs.weekDays);
|
||||
if (attrs.snapInterval) this.config.snapInterval = parseInt(attrs.snapInterval);
|
||||
if (attrs.dayStartHour) this.config.dayStartHour = parseInt(attrs.dayStartHour);
|
||||
if (attrs.dayEndHour) this.config.dayEndHour = parseInt(attrs.dayEndHour);
|
||||
if (attrs.hourHeight) this.config.hourHeight = parseInt(attrs.hourHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a config value
|
||||
* @param {string} key
|
||||
* @returns {*}
|
||||
*/
|
||||
get(key) {
|
||||
return this.config[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a config value
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
*/
|
||||
set(key, value) {
|
||||
const oldValue = this.config[key];
|
||||
this.config[key] = value;
|
||||
|
||||
// Update computed values
|
||||
if (key === 'snapInterval') {
|
||||
this.config.minEventDuration = value;
|
||||
}
|
||||
|
||||
// Emit config update event
|
||||
eventBus.emit(EventTypes.CONFIG_UPDATE, {
|
||||
key,
|
||||
value,
|
||||
oldValue
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update multiple config values
|
||||
* @param {Object} updates
|
||||
*/
|
||||
update(updates) {
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
this.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all config
|
||||
* @returns {Object}
|
||||
*/
|
||||
getAll() {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate derived values
|
||||
*/
|
||||
|
||||
get minuteHeight() {
|
||||
return this.config.hourHeight / 60;
|
||||
}
|
||||
|
||||
get totalHours() {
|
||||
return this.config.dayEndHour - this.config.dayStartHour;
|
||||
}
|
||||
|
||||
get totalMinutes() {
|
||||
return this.totalHours * 60;
|
||||
}
|
||||
|
||||
get slotsPerHour() {
|
||||
return 60 / this.config.snapInterval;
|
||||
}
|
||||
|
||||
get totalSlots() {
|
||||
return this.totalHours * this.slotsPerHour;
|
||||
}
|
||||
|
||||
get slotHeight() {
|
||||
return this.config.hourHeight / this.slotsPerHour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate snap interval
|
||||
* @param {number} interval
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isValidSnapInterval(interval) {
|
||||
return [5, 10, 15, 30, 60].includes(interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get view-specific settings
|
||||
* @param {string} view
|
||||
* @returns {Object}
|
||||
*/
|
||||
getViewSettings(view = this.config.view) {
|
||||
const settings = {
|
||||
day: {
|
||||
columns: 1,
|
||||
showAllDay: true,
|
||||
scrollToHour: 8
|
||||
},
|
||||
week: {
|
||||
columns: this.config.weekDays,
|
||||
showAllDay: true,
|
||||
scrollToHour: 8
|
||||
},
|
||||
month: {
|
||||
columns: 7,
|
||||
showAllDay: false,
|
||||
scrollToHour: null
|
||||
}
|
||||
};
|
||||
|
||||
return settings[view] || settings.week;
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const calendarConfig = new CalendarConfig();
|
||||
385
.workbench/poc-layouts/calendar-data-manager.js
Normal file
385
.workbench/poc-layouts/calendar-data-manager.js
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
// js/managers/DataManager.js
|
||||
|
||||
import { eventBus } from '../core/EventBus.js';
|
||||
import { EventTypes } from '../types/EventTypes.js';
|
||||
|
||||
/**
|
||||
* Manages data fetching and API communication
|
||||
* Currently uses mock data until backend is implemented
|
||||
*/
|
||||
export class DataManager {
|
||||
constructor() {
|
||||
this.baseUrl = '/api/events';
|
||||
this.useMockData = true; // Toggle this when backend is ready
|
||||
this.cache = new Map();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.subscribeToEvents();
|
||||
}
|
||||
|
||||
subscribeToEvents() {
|
||||
// Listen for period changes to fetch new data
|
||||
eventBus.on(EventTypes.PERIOD_CHANGE, (e) => {
|
||||
this.fetchEventsForPeriod(e.detail);
|
||||
});
|
||||
|
||||
// Listen for event updates
|
||||
eventBus.on(EventTypes.EVENT_UPDATE, (e) => {
|
||||
this.updateEvent(e.detail);
|
||||
});
|
||||
|
||||
// Listen for event creation
|
||||
eventBus.on(EventTypes.EVENT_CREATE, (e) => {
|
||||
this.createEvent(e.detail);
|
||||
});
|
||||
|
||||
// Listen for event deletion
|
||||
eventBus.on(EventTypes.EVENT_DELETE, (e) => {
|
||||
this.deleteEvent(e.detail.eventId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch events for a specific period
|
||||
* @param {Object} period - Contains start, end, view
|
||||
*/
|
||||
async fetchEventsForPeriod(period) {
|
||||
const cacheKey = `${period.start}-${period.end}-${period.view}`;
|
||||
|
||||
// Check cache first
|
||||
if (this.cache.has(cacheKey)) {
|
||||
const cachedData = this.cache.get(cacheKey);
|
||||
eventBus.emit(EventTypes.DATA_FETCH_SUCCESS, cachedData);
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
// Emit loading start
|
||||
eventBus.emit(EventTypes.DATA_FETCH_START, { period });
|
||||
|
||||
try {
|
||||
let data;
|
||||
|
||||
if (this.useMockData) {
|
||||
// Simulate network delay
|
||||
await this.delay(300);
|
||||
data = this.getMockData(period);
|
||||
} else {
|
||||
// Real API call
|
||||
const params = new URLSearchParams({
|
||||
start: period.start,
|
||||
end: period.end,
|
||||
view: period.view
|
||||
});
|
||||
|
||||
const response = await fetch(`${this.baseUrl}?${params}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch events');
|
||||
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
// Cache the data
|
||||
this.cache.set(cacheKey, data);
|
||||
|
||||
// Emit success
|
||||
eventBus.emit(EventTypes.DATA_FETCH_SUCCESS, data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
eventBus.emit(EventTypes.DATA_FETCH_ERROR, { error: error.message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new event
|
||||
*/
|
||||
async createEvent(eventData) {
|
||||
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'create' });
|
||||
|
||||
try {
|
||||
if (this.useMockData) {
|
||||
await this.delay(200);
|
||||
const newEvent = {
|
||||
id: `evt-${Date.now()}`,
|
||||
...eventData,
|
||||
syncStatus: 'synced'
|
||||
};
|
||||
|
||||
// Clear cache to force refresh
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'create',
|
||||
event: newEvent
|
||||
});
|
||||
|
||||
return newEvent;
|
||||
} else {
|
||||
// Real API call
|
||||
const response = await fetch(this.baseUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(eventData)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to create event');
|
||||
|
||||
const newEvent = await response.json();
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'create',
|
||||
event: newEvent
|
||||
});
|
||||
|
||||
return newEvent;
|
||||
}
|
||||
} catch (error) {
|
||||
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
|
||||
action: 'create',
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing event
|
||||
*/
|
||||
async updateEvent(updateData) {
|
||||
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'update' });
|
||||
|
||||
try {
|
||||
if (this.useMockData) {
|
||||
await this.delay(200);
|
||||
|
||||
// Clear cache to force refresh
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'update',
|
||||
eventId: updateData.eventId,
|
||||
changes: updateData.changes
|
||||
});
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// Real API call
|
||||
const response = await fetch(`${this.baseUrl}/${updateData.eventId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updateData.changes)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to update event');
|
||||
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'update',
|
||||
eventId: updateData.eventId
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
|
||||
action: 'update',
|
||||
error: error.message,
|
||||
eventId: updateData.eventId
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an event
|
||||
*/
|
||||
async deleteEvent(eventId) {
|
||||
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'delete' });
|
||||
|
||||
try {
|
||||
if (this.useMockData) {
|
||||
await this.delay(200);
|
||||
|
||||
// Clear cache to force refresh
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'delete',
|
||||
eventId
|
||||
});
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// Real API call
|
||||
const response = await fetch(`${this.baseUrl}/${eventId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to delete event');
|
||||
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'delete',
|
||||
eventId
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
|
||||
action: 'delete',
|
||||
error: error.message,
|
||||
eventId
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate mock data for testing
|
||||
*/
|
||||
getMockData(period) {
|
||||
const events = [];
|
||||
const types = ['meeting', 'meal', 'work', 'milestone'];
|
||||
const titles = {
|
||||
meeting: ['Team Standup', 'Client Meeting', 'Project Review', 'Sprint Planning', 'Design Review'],
|
||||
meal: ['Breakfast', 'Lunch', 'Coffee Break', 'Dinner'],
|
||||
work: ['Deep Work Session', 'Code Review', 'Documentation', 'Testing'],
|
||||
milestone: ['Project Deadline', 'Release Day', 'Demo Day']
|
||||
};
|
||||
|
||||
// Parse dates
|
||||
const startDate = new Date(period.start);
|
||||
const endDate = new Date(period.end);
|
||||
|
||||
// Generate some events for each day
|
||||
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
||||
// Skip weekends for most events
|
||||
const dayOfWeek = d.getDay();
|
||||
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
||||
|
||||
if (isWeekend) {
|
||||
// Maybe one or two events on weekends
|
||||
if (Math.random() > 0.7) {
|
||||
const type = 'meal';
|
||||
const title = titles[type][Math.floor(Math.random() * titles[type].length)];
|
||||
const hour = 12 + Math.floor(Math.random() * 4);
|
||||
|
||||
events.push({
|
||||
id: `evt-${events.length + 1}`,
|
||||
title,
|
||||
type,
|
||||
start: `${this.formatDate(d)}T${hour}:00:00`,
|
||||
end: `${this.formatDate(d)}T${hour + 1}:00:00`,
|
||||
allDay: false,
|
||||
syncStatus: 'synced'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Regular workday events
|
||||
|
||||
// Morning standup
|
||||
if (Math.random() > 0.3) {
|
||||
events.push({
|
||||
id: `evt-${events.length + 1}`,
|
||||
title: 'Team Standup',
|
||||
type: 'meeting',
|
||||
start: `${this.formatDate(d)}T09:00:00`,
|
||||
end: `${this.formatDate(d)}T09:30:00`,
|
||||
allDay: false,
|
||||
syncStatus: 'synced'
|
||||
});
|
||||
}
|
||||
|
||||
// Lunch
|
||||
events.push({
|
||||
id: `evt-${events.length + 1}`,
|
||||
title: 'Lunch',
|
||||
type: 'meal',
|
||||
start: `${this.formatDate(d)}T12:00:00`,
|
||||
end: `${this.formatDate(d)}T13:00:00`,
|
||||
allDay: false,
|
||||
syncStatus: 'synced'
|
||||
});
|
||||
|
||||
// Random afternoon events
|
||||
const numAfternoonEvents = Math.floor(Math.random() * 3) + 1;
|
||||
for (let i = 0; i < numAfternoonEvents; i++) {
|
||||
const type = types[Math.floor(Math.random() * types.length)];
|
||||
const title = titles[type][Math.floor(Math.random() * titles[type].length)];
|
||||
const startHour = 13 + Math.floor(Math.random() * 4);
|
||||
const duration = 1 + Math.floor(Math.random() * 2);
|
||||
|
||||
events.push({
|
||||
id: `evt-${events.length + 1}`,
|
||||
title,
|
||||
type,
|
||||
start: `${this.formatDate(d)}T${startHour}:${Math.random() > 0.5 ? '00' : '30'}:00`,
|
||||
end: `${this.formatDate(d)}T${startHour + duration}:00:00`,
|
||||
allDay: false,
|
||||
syncStatus: Math.random() > 0.9 ? 'pending' : 'synced'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a multi-day event
|
||||
if (period.view === 'week') {
|
||||
const midWeek = new Date(startDate);
|
||||
midWeek.setDate(midWeek.getDate() + 2);
|
||||
|
||||
events.push({
|
||||
id: `evt-${events.length + 1}`,
|
||||
title: 'Project Sprint',
|
||||
type: 'milestone',
|
||||
start: `${this.formatDate(startDate)}T00:00:00`,
|
||||
end: `${this.formatDate(midWeek)}T23:59:59`,
|
||||
allDay: true,
|
||||
syncStatus: 'synced'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
events,
|
||||
meta: {
|
||||
start: period.start,
|
||||
end: period.end,
|
||||
view: period.view,
|
||||
total: events.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility methods
|
||||
*/
|
||||
|
||||
formatDate(date) {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached data
|
||||
*/
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between mock and real data
|
||||
*/
|
||||
setUseMockData(useMock) {
|
||||
this.useMockData = useMock;
|
||||
this.clearCache();
|
||||
}
|
||||
}
|
||||
231
.workbench/poc-layouts/calendar-date-utils.js
Normal file
231
.workbench/poc-layouts/calendar-date-utils.js
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// js/utils/DateUtils.js
|
||||
|
||||
/**
|
||||
* Date and time utility functions
|
||||
*/
|
||||
export class DateUtils {
|
||||
/**
|
||||
* Get start of week for a given date
|
||||
* @param {Date} date
|
||||
* @param {number} firstDayOfWeek - 0 = Sunday, 1 = Monday
|
||||
* @returns {Date}
|
||||
*/
|
||||
static getWeekStart(date, firstDayOfWeek = 1) {
|
||||
const d = new Date(date);
|
||||
const day = d.getDay();
|
||||
const diff = (day - firstDayOfWeek + 7) % 7;
|
||||
d.setDate(d.getDate() - diff);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get end of week for a given date
|
||||
* @param {Date} date
|
||||
* @param {number} firstDayOfWeek
|
||||
* @returns {Date}
|
||||
*/
|
||||
static getWeekEnd(date, firstDayOfWeek = 1) {
|
||||
const start = this.getWeekStart(date, firstDayOfWeek);
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 6);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
return end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date to YYYY-MM-DD
|
||||
* @param {Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
static formatDate(date) {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time to HH:MM
|
||||
* @param {Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
static formatTime(date) {
|
||||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time to 12-hour format
|
||||
* @param {Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
static formatTime12(date) {
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
const displayHours = hours % 12 || 12;
|
||||
|
||||
return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert minutes since midnight to time string
|
||||
* @param {number} minutes
|
||||
* @returns {string}
|
||||
*/
|
||||
static minutesToTime(minutes) {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
const displayHours = hours % 12 || 12;
|
||||
|
||||
return `${displayHours}:${String(mins).padStart(2, '0')} ${period}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert time string to minutes since midnight
|
||||
* @param {string} timeStr - Format: "HH:MM" or "HH:MM:SS"
|
||||
* @returns {number}
|
||||
*/
|
||||
static timeToMinutes(timeStr) {
|
||||
const [time] = timeStr.split('T').pop().split('.');
|
||||
const [hours, minutes] = time.split(':').map(Number);
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minutes since start of day
|
||||
* @param {Date|string} date
|
||||
* @returns {number}
|
||||
*/
|
||||
static getMinutesSinceMidnight(date) {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return d.getHours() * 60 + d.getMinutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate duration in minutes between two dates
|
||||
* @param {Date|string} start
|
||||
* @param {Date|string} end
|
||||
* @returns {number}
|
||||
*/
|
||||
static getDurationMinutes(start, end) {
|
||||
const startDate = typeof start === 'string' ? new Date(start) : start;
|
||||
const endDate = typeof end === 'string' ? new Date(end) : end;
|
||||
return Math.floor((endDate - startDate) / 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if date is today
|
||||
* @param {Date} date
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isToday(date) {
|
||||
const today = new Date();
|
||||
return date.toDateString() === today.toDateString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two dates are on the same day
|
||||
* @param {Date} date1
|
||||
* @param {Date} date2
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isSameDay(date1, date2) {
|
||||
return date1.toDateString() === date2.toDateString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event spans multiple days
|
||||
* @param {Date|string} start
|
||||
* @param {Date|string} end
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isMultiDay(start, end) {
|
||||
const startDate = typeof start === 'string' ? new Date(start) : start;
|
||||
const endDate = typeof end === 'string' ? new Date(end) : end;
|
||||
return !this.isSameDay(startDate, endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get day name
|
||||
* @param {Date} date
|
||||
* @param {string} format - 'short' or 'long'
|
||||
* @returns {string}
|
||||
*/
|
||||
static getDayName(date, format = 'short') {
|
||||
const days = {
|
||||
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||
long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
||||
};
|
||||
return days[format][date.getDay()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add days to date
|
||||
* @param {Date} date
|
||||
* @param {number} days
|
||||
* @returns {Date}
|
||||
*/
|
||||
static addDays(date, days) {
|
||||
const result = new Date(date);
|
||||
result.setDate(result.getDate() + days);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add minutes to date
|
||||
* @param {Date} date
|
||||
* @param {number} minutes
|
||||
* @returns {Date}
|
||||
*/
|
||||
static addMinutes(date, minutes) {
|
||||
const result = new Date(date);
|
||||
result.setMinutes(result.getMinutes() + minutes);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap time to nearest interval
|
||||
* @param {Date} date
|
||||
* @param {number} intervalMinutes
|
||||
* @returns {Date}
|
||||
*/
|
||||
static snapToInterval(date, intervalMinutes) {
|
||||
const minutes = date.getMinutes();
|
||||
const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes;
|
||||
const result = new Date(date);
|
||||
result.setMinutes(snappedMinutes);
|
||||
result.setSeconds(0);
|
||||
result.setMilliseconds(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current time in minutes since day start
|
||||
* @param {number} dayStartHour
|
||||
* @returns {number}
|
||||
*/
|
||||
static getCurrentTimeMinutes(dayStartHour = 0) {
|
||||
const now = new Date();
|
||||
const minutesSinceMidnight = now.getHours() * 60 + now.getMinutes();
|
||||
return minutesSinceMidnight - (dayStartHour * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration to human readable string
|
||||
* @param {number} minutes
|
||||
* @returns {string}
|
||||
*/
|
||||
static formatDuration(minutes) {
|
||||
if (minutes < 60) {
|
||||
return `${minutes} min`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
|
||||
if (mins === 0) {
|
||||
return `${hours} hour${hours > 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
return `${hours} hour${hours > 1 ? 's' : ''} ${mins} min`;
|
||||
}
|
||||
}
|
||||
73
.workbench/poc-layouts/calendar-event-types.js
Normal file
73
.workbench/poc-layouts/calendar-event-types.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// js/types/EventTypes.js
|
||||
|
||||
/**
|
||||
* Calendar event type constants
|
||||
*/
|
||||
export const EventTypes = {
|
||||
// View events
|
||||
VIEW_CHANGE: 'calendar:viewchange',
|
||||
VIEW_RENDERED: 'calendar:viewrendered',
|
||||
PERIOD_CHANGE: 'calendar:periodchange',
|
||||
|
||||
// Event CRUD
|
||||
EVENT_CREATE: 'calendar:eventcreate',
|
||||
EVENT_UPDATE: 'calendar:eventupdate',
|
||||
EVENT_DELETE: 'calendar:eventdelete',
|
||||
EVENT_RENDERED: 'calendar:eventrendered',
|
||||
EVENT_SELECTED: 'calendar:eventselected',
|
||||
|
||||
// Interaction events
|
||||
DRAG_START: 'calendar:dragstart',
|
||||
DRAG_MOVE: 'calendar:dragmove',
|
||||
DRAG_END: 'calendar:dragend',
|
||||
DRAG_CANCEL: 'calendar:dragcancel',
|
||||
|
||||
RESIZE_START: 'calendar:resizestart',
|
||||
RESIZE_MOVE: 'calendar:resizemove',
|
||||
RESIZE_END: 'calendar:resizeend',
|
||||
RESIZE_CANCEL: 'calendar:resizecancel',
|
||||
|
||||
// UI events
|
||||
POPUP_SHOW: 'calendar:popupshow',
|
||||
POPUP_HIDE: 'calendar:popuphide',
|
||||
|
||||
SEARCH_START: 'calendar:searchstart',
|
||||
SEARCH_UPDATE: 'calendar:searchupdate',
|
||||
SEARCH_CLEAR: 'calendar:searchclear',
|
||||
|
||||
// Grid events
|
||||
GRID_CLICK: 'calendar:gridclick',
|
||||
GRID_DBLCLICK: 'calendar:griddblclick',
|
||||
GRID_RENDERED: 'calendar:gridrendered',
|
||||
|
||||
// Data events
|
||||
DATA_FETCH_START: 'calendar:datafetchstart',
|
||||
DATA_FETCH_SUCCESS: 'calendar:datafetchsuccess',
|
||||
DATA_FETCH_ERROR: 'calendar:datafetcherror',
|
||||
DATA_SYNC_START: 'calendar:datasyncstart',
|
||||
DATA_SYNC_SUCCESS: 'calendar:datasyncsuccess',
|
||||
DATA_SYNC_ERROR: 'calendar:datasyncerror',
|
||||
|
||||
// State events
|
||||
STATE_UPDATE: 'calendar:stateupdate',
|
||||
CONFIG_UPDATE: 'calendar:configupdate',
|
||||
|
||||
// Time events
|
||||
TIME_UPDATE: 'calendar:timeupdate',
|
||||
|
||||
// Navigation events
|
||||
NAV_PREV: 'calendar:navprev',
|
||||
NAV_NEXT: 'calendar:navnext',
|
||||
NAV_TODAY: 'calendar:navtoday',
|
||||
|
||||
// Loading events
|
||||
LOADING_START: 'calendar:loadingstart',
|
||||
LOADING_END: 'calendar:loadingend',
|
||||
|
||||
// Error events
|
||||
ERROR: 'calendar:error',
|
||||
|
||||
// Init events
|
||||
READY: 'calendar:ready',
|
||||
DESTROY: 'calendar:destroy'
|
||||
};
|
||||
115
.workbench/poc-layouts/calendar-eventbus.js
Normal file
115
.workbench/poc-layouts/calendar-eventbus.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// js/core/EventBus.js
|
||||
|
||||
/**
|
||||
* Central event dispatcher for calendar using DOM CustomEvents
|
||||
* Provides logging and debugging capabilities
|
||||
*/
|
||||
export class EventBus {
|
||||
constructor() {
|
||||
this.eventLog = [];
|
||||
this.debug = false; // Set to true for console logging
|
||||
this.listeners = new Set(); // Track listeners for cleanup
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event via DOM addEventListener
|
||||
* @param {string} eventType
|
||||
* @param {Function} handler
|
||||
* @param {Object} options
|
||||
* @returns {Function} Unsubscribe function
|
||||
*/
|
||||
on(eventType, handler, options = {}) {
|
||||
document.addEventListener(eventType, handler, options);
|
||||
|
||||
// Track for cleanup
|
||||
this.listeners.add({ eventType, handler, options });
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => this.off(eventType, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event once
|
||||
* @param {string} eventType
|
||||
* @param {Function} handler
|
||||
*/
|
||||
once(eventType, handler) {
|
||||
return this.on(eventType, handler, { once: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from an event
|
||||
* @param {string} eventType
|
||||
* @param {Function} handler
|
||||
*/
|
||||
off(eventType, handler) {
|
||||
document.removeEventListener(eventType, handler);
|
||||
|
||||
// Remove from tracking
|
||||
for (const listener of this.listeners) {
|
||||
if (listener.eventType === eventType && listener.handler === handler) {
|
||||
this.listeners.delete(listener);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event via DOM CustomEvent
|
||||
* @param {string} eventType
|
||||
* @param {*} detail
|
||||
* @returns {boolean} Whether event was cancelled
|
||||
*/
|
||||
emit(eventType, detail = {}) {
|
||||
const event = new CustomEvent(eventType, {
|
||||
detail,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
|
||||
|
||||
|
||||
this.eventLog.push({
|
||||
type: eventType,
|
||||
detail,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Emit on document (only DOM events now)
|
||||
return !document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event history
|
||||
* @param {string} eventType Optional filter by type
|
||||
* @returns {Array}
|
||||
*/
|
||||
getEventLog(eventType = null) {
|
||||
if (eventType) {
|
||||
return this.eventLog.filter(e => e.type === eventType);
|
||||
}
|
||||
return this.eventLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable debug mode
|
||||
* @param {boolean} enabled
|
||||
*/
|
||||
setDebug(enabled) {
|
||||
this.debug = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all tracked listeners
|
||||
*/
|
||||
destroy() {
|
||||
for (const listener of this.listeners) {
|
||||
document.removeEventListener(listener.eventType, listener.handler);
|
||||
}
|
||||
this.listeners.clear();
|
||||
this.eventLog = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const eventBus = new EventBus();
|
||||
334
.workbench/poc-layouts/calendar-grid-manager.js
Normal file
334
.workbench/poc-layouts/calendar-grid-manager.js
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
// js/managers/GridManager.js
|
||||
|
||||
import { eventBus } from '../core/EventBus.js';
|
||||
import { calendarConfig } from '../core/CalendarConfig.js';
|
||||
import { EventTypes } from '../types/EventTypes.js';
|
||||
import { DateUtils } from '../utils/DateUtils.js';
|
||||
|
||||
/**
|
||||
* Manages the calendar grid structure
|
||||
*/
|
||||
export class GridManager {
|
||||
constructor() {
|
||||
this.container = null;
|
||||
this.timeAxis = null;
|
||||
this.weekHeader = null;
|
||||
this.timeGrid = null;
|
||||
this.dayColumns = null;
|
||||
this.currentWeek = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.findElements();
|
||||
this.subscribeToEvents();
|
||||
}
|
||||
|
||||
findElements() {
|
||||
this.container = document.querySelector('swp-calendar-container');
|
||||
this.timeAxis = document.querySelector('swp-time-axis');
|
||||
this.weekHeader = document.querySelector('swp-calendar-header');
|
||||
this.timeGrid = document.querySelector('swp-time-grid');
|
||||
this.scrollableContent = document.querySelector('swp-scrollable-content');
|
||||
}
|
||||
|
||||
subscribeToEvents() {
|
||||
// Re-render grid on config changes
|
||||
eventBus.on(EventTypes.CONFIG_UPDATE, (e) => {
|
||||
if (['dayStartHour', 'dayEndHour', 'hourHeight', 'view', 'weekDays'].includes(e.detail.key)) {
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
|
||||
// Re-render on view change
|
||||
eventBus.on(EventTypes.VIEW_CHANGE, () => {
|
||||
this.render();
|
||||
});
|
||||
|
||||
// Re-render on period change
|
||||
eventBus.on(EventTypes.PERIOD_CHANGE, (e) => {
|
||||
this.currentWeek = e.detail.week;
|
||||
this.renderHeaders();
|
||||
});
|
||||
|
||||
// Handle grid clicks
|
||||
this.setupGridInteractions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the complete grid structure
|
||||
*/
|
||||
render() {
|
||||
this.renderTimeAxis();
|
||||
this.renderHeaders();
|
||||
this.renderGrid();
|
||||
this.renderGridLines();
|
||||
|
||||
// Emit grid rendered event
|
||||
eventBus.emit(EventTypes.GRID_RENDERED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render time axis (left side hours)
|
||||
*/
|
||||
renderTimeAxis() {
|
||||
if (!this.timeAxis) return;
|
||||
|
||||
const startHour = calendarConfig.get('dayStartHour');
|
||||
const endHour = calendarConfig.get('dayEndHour');
|
||||
|
||||
this.timeAxis.innerHTML = '';
|
||||
|
||||
for (let hour = startHour; hour <= endHour; hour++) {
|
||||
const marker = document.createElement('swp-hour-marker');
|
||||
marker.textContent = this.formatHour(hour);
|
||||
marker.dataset.hour = hour;
|
||||
this.timeAxis.appendChild(marker);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render week headers
|
||||
*/
|
||||
renderHeaders() {
|
||||
if (!this.weekHeader || !this.currentWeek) return;
|
||||
|
||||
const view = calendarConfig.get('view');
|
||||
const weekDays = calendarConfig.get('weekDays');
|
||||
|
||||
this.weekHeader.innerHTML = '';
|
||||
|
||||
if (view === 'week') {
|
||||
const dates = this.getWeekDates(this.currentWeek);
|
||||
const daysToShow = dates.slice(0, weekDays);
|
||||
|
||||
daysToShow.forEach((date, index) => {
|
||||
const header = document.createElement('swp-day-header');
|
||||
header.innerHTML = `
|
||||
<swp-day-name>${this.getDayName(date)}</swp-day-name>
|
||||
<swp-day-date>${date.getDate()}</swp-day-date>
|
||||
`;
|
||||
header.dataset.date = this.formatDate(date);
|
||||
header.dataset.dayIndex = index;
|
||||
|
||||
// Mark today
|
||||
if (this.isToday(date)) {
|
||||
header.dataset.today = 'true';
|
||||
}
|
||||
|
||||
this.weekHeader.appendChild(header);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the main grid structure
|
||||
*/
|
||||
renderGrid() {
|
||||
if (!this.timeGrid) return;
|
||||
|
||||
// Clear existing columns
|
||||
let dayColumns = this.timeGrid.querySelector('swp-day-columns');
|
||||
if (!dayColumns) {
|
||||
dayColumns = document.createElement('swp-day-columns');
|
||||
this.timeGrid.appendChild(dayColumns);
|
||||
}
|
||||
|
||||
dayColumns.innerHTML = '';
|
||||
|
||||
const view = calendarConfig.get('view');
|
||||
const columnsCount = view === 'week' ? calendarConfig.get('weekDays') : 1;
|
||||
|
||||
// Create columns
|
||||
for (let i = 0; i < columnsCount; i++) {
|
||||
const column = document.createElement('swp-day-column');
|
||||
column.dataset.columnIndex = i;
|
||||
|
||||
if (this.currentWeek) {
|
||||
const dates = this.getWeekDates(this.currentWeek);
|
||||
if (dates[i]) {
|
||||
column.dataset.date = this.formatDate(dates[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add events container
|
||||
const eventsLayer = document.createElement('swp-events-layer');
|
||||
column.appendChild(eventsLayer);
|
||||
|
||||
dayColumns.appendChild(column);
|
||||
}
|
||||
|
||||
this.dayColumns = dayColumns;
|
||||
this.updateGridStyles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render grid lines
|
||||
*/
|
||||
renderGridLines() {
|
||||
let gridLines = this.timeGrid.querySelector('swp-grid-lines');
|
||||
if (!gridLines) {
|
||||
gridLines = document.createElement('swp-grid-lines');
|
||||
this.timeGrid.insertBefore(gridLines, this.timeGrid.firstChild);
|
||||
}
|
||||
|
||||
const totalHours = calendarConfig.totalHours;
|
||||
const hourHeight = calendarConfig.get('hourHeight');
|
||||
|
||||
// Set CSS variables
|
||||
this.timeGrid.style.setProperty('--total-hours', totalHours);
|
||||
this.timeGrid.style.setProperty('--hour-height', `${hourHeight}px`);
|
||||
|
||||
// Grid lines are handled by CSS
|
||||
}
|
||||
|
||||
/**
|
||||
* Update grid CSS variables
|
||||
*/
|
||||
updateGridStyles() {
|
||||
const root = document.documentElement;
|
||||
const config = calendarConfig.getAll();
|
||||
|
||||
// Set CSS variables
|
||||
root.style.setProperty('--hour-height', `${config.hourHeight}px`);
|
||||
root.style.setProperty('--minute-height', `${config.hourHeight / 60}px`);
|
||||
root.style.setProperty('--snap-interval', config.snapInterval);
|
||||
root.style.setProperty('--day-start-hour', config.dayStartHour);
|
||||
root.style.setProperty('--day-end-hour', config.dayEndHour);
|
||||
root.style.setProperty('--work-start-hour', config.workStartHour);
|
||||
root.style.setProperty('--work-end-hour', config.workEndHour);
|
||||
|
||||
// Set grid height
|
||||
const totalHeight = calendarConfig.totalHours * config.hourHeight;
|
||||
if (this.timeGrid) {
|
||||
this.timeGrid.style.height = `${totalHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup grid interaction handlers
|
||||
*/
|
||||
setupGridInteractions() {
|
||||
if (!this.timeGrid) return;
|
||||
|
||||
// Click handler
|
||||
this.timeGrid.addEventListener('click', (e) => {
|
||||
// Ignore if clicking on an event
|
||||
if (e.target.closest('swp-event')) return;
|
||||
|
||||
const column = e.target.closest('swp-day-column');
|
||||
if (!column) return;
|
||||
|
||||
const position = this.getClickPosition(e, column);
|
||||
|
||||
eventBus.emit(EventTypes.GRID_CLICK, {
|
||||
date: column.dataset.date,
|
||||
time: position.time,
|
||||
minutes: position.minutes,
|
||||
columnIndex: parseInt(column.dataset.columnIndex)
|
||||
});
|
||||
});
|
||||
|
||||
// Double click handler
|
||||
this.timeGrid.addEventListener('dblclick', (e) => {
|
||||
// Ignore if clicking on an event
|
||||
if (e.target.closest('swp-event')) return;
|
||||
|
||||
const column = e.target.closest('swp-day-column');
|
||||
if (!column) return;
|
||||
|
||||
const position = this.getClickPosition(e, column);
|
||||
|
||||
eventBus.emit(EventTypes.GRID_DBLCLICK, {
|
||||
date: column.dataset.date,
|
||||
time: position.time,
|
||||
minutes: position.minutes,
|
||||
columnIndex: parseInt(column.dataset.columnIndex)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get click position in grid
|
||||
*/
|
||||
getClickPosition(event, column) {
|
||||
const rect = column.getBoundingClientRect();
|
||||
const y = event.clientY - rect.top + this.scrollableContent.scrollTop;
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility methods
|
||||
*/
|
||||
|
||||
formatHour(hour) {
|
||||
const period = hour >= 12 ? 'PM' : 'AM';
|
||||
const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);
|
||||
return `${displayHour} ${period}`;
|
||||
}
|
||||
|
||||
formatDate(date) {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
getDayName(date) {
|
||||
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
return days[date.getDay()];
|
||||
}
|
||||
|
||||
getWeekDates(weekStart) {
|
||||
const dates = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(weekStart);
|
||||
date.setDate(weekStart.getDate() + i);
|
||||
dates.push(date);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
isToday(date) {
|
||||
const today = new Date();
|
||||
return date.toDateString() === today.toDateString();
|
||||
}
|
||||
|
||||
minutesToTime(totalMinutes) {
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
const displayHour = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
|
||||
|
||||
return `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to specific hour
|
||||
*/
|
||||
scrollToHour(hour) {
|
||||
if (!this.scrollableContent) return;
|
||||
|
||||
const hourHeight = calendarConfig.get('hourHeight');
|
||||
const dayStartHour = calendarConfig.get('dayStartHour');
|
||||
const scrollTop = (hour - dayStartHour) * hourHeight;
|
||||
|
||||
this.scrollableContent.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
1066
.workbench/poc-layouts/calendar-poc-single-file.html
Normal file
1066
.workbench/poc-layouts/calendar-poc-single-file.html
Normal file
File diff suppressed because it is too large
Load diff
536
.workbench/poc-layouts/month-view-design.html
Normal file
536
.workbench/poc-layouts/month-view-design.html
Normal file
|
|
@ -0,0 +1,536 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Calendar Plantempus - Month View</title>
|
||||
<style>
|
||||
/* Use existing Calendar Plantempus variables and styling */
|
||||
:root {
|
||||
--hour-height: 60px;
|
||||
--minute-height: 1px;
|
||||
--snap-interval: 15;
|
||||
--day-column-min-width: 140px;
|
||||
--week-days: 7;
|
||||
--header-height: 80px;
|
||||
|
||||
--color-primary: #2196f3;
|
||||
--color-secondary: #ff9800;
|
||||
--color-success: #4caf50;
|
||||
--color-warning: #ff5722;
|
||||
--color-error: #f44336;
|
||||
--color-text: #2c3e50;
|
||||
--color-text-secondary: #6c757d;
|
||||
--color-border: #e0e0e0;
|
||||
--color-background: #ffffff;
|
||||
--color-surface: #f8f9fa;
|
||||
--color-hover: #f0f0f0;
|
||||
|
||||
--transition-fast: 0.15s ease;
|
||||
--border-radius: 4px;
|
||||
--box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Month grid container - matches existing swp-calendar-container */
|
||||
.month-container {
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
/* Month grid layout with week numbers */
|
||||
.month-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 40px repeat(7, 1fr); /* Small column for week numbers + 7 days */
|
||||
grid-template-rows: 40px repeat(6, 1fr);
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
/* Week number header */
|
||||
.week-header {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
background: var(--color-surface);
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* Day headers - only day names, right aligned, smaller height */
|
||||
.month-day-header {
|
||||
background: var(--color-surface);
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 8px 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.month-day-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Week number cells */
|
||||
.week-number {
|
||||
grid-column: 1;
|
||||
background: var(--color-surface);
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Month day cells - similar to existing day columns */
|
||||
.month-day-cell {
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 8px;
|
||||
background: var(--color-background);
|
||||
transition: background-color var(--transition-fast);
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.month-day-cell:hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
.month-day-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.month-day-cell.other-month {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.month-day-cell.today {
|
||||
background: #f0f8ff;
|
||||
border-left: 3px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.month-day-cell.weekend {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.month-day-number {
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.month-day-cell.today .month-day-number {
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Month events styling - compact version of existing events */
|
||||
.month-events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
max-height: 70px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.month-event {
|
||||
background: #e3f2fd;
|
||||
color: var(--color-primary);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
border-left: 2px solid var(--color-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.month-event:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Event categories - using existing color scheme */
|
||||
.month-event.category-meeting {
|
||||
background: #e8f5e8;
|
||||
color: var(--color-success);
|
||||
border-left-color: var(--color-success);
|
||||
}
|
||||
|
||||
.month-event.category-deadline {
|
||||
background: #ffebee;
|
||||
color: var(--color-error);
|
||||
border-left-color: var(--color-error);
|
||||
}
|
||||
|
||||
.month-event.category-work {
|
||||
background: #fff8e1;
|
||||
color: var(--color-secondary);
|
||||
border-left-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.month-event.category-personal {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
border-left-color: #9c27b0;
|
||||
}
|
||||
|
||||
.month-event-more {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-size: 9px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border: 1px dashed var(--color-border);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.month-event-more:hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.month-container {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.month-grid {
|
||||
grid-template-columns: 30px repeat(7, 1fr);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.month-day-cell {
|
||||
min-height: 60px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.month-day-header {
|
||||
padding: 8px 4px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.month-event {
|
||||
font-size: 9px;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
.month-events {
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
.week-number {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="month-container">
|
||||
<div class="month-grid">
|
||||
<!-- Week number header -->
|
||||
<div class="week-header">Uge</div>
|
||||
|
||||
<!-- Day headers - only day names, right aligned -->
|
||||
<div class="month-day-header">Man</div>
|
||||
<div class="month-day-header">Tir</div>
|
||||
<div class="month-day-header">Ons</div>
|
||||
<div class="month-day-header">Tor</div>
|
||||
<div class="month-day-header">Fre</div>
|
||||
<div class="month-day-header">Lør</div>
|
||||
<div class="month-day-header">Søn</div>
|
||||
|
||||
<!-- Week 1 -->
|
||||
<div class="week-number">1</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">30</div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">31</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal">Nytår</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">1</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal">Nytårsdag</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">2</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work">Tilbage på arbejde</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">3</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting">Team møde</div>
|
||||
<div class="month-event category-work">Review</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">4</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">5</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal">Familie</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Week 2 -->
|
||||
<div class="week-number">2</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">6</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting">Stand-up</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">7</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-deadline">Rapport</div>
|
||||
<div class="month-event category-meeting">1:1</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">8</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work">Code review</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">9</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting">Planning</div>
|
||||
<div class="month-event category-work">Design</div>
|
||||
<div class="month-event-more">+1</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">10</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work">Sprint review</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">11</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">12</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal">Brunch</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Week 3 -->
|
||||
<div class="week-number">3</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">13</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting">All hands</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">14</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work">Deploy</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">15</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-deadline">Presentation</div>
|
||||
<div class="month-event category-meeting">Retro</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">16</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work">Bug triage</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">17</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting">Planning</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">18</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal">Koncert</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">19</div>
|
||||
</div>
|
||||
|
||||
<!-- Week 4 - Today -->
|
||||
<div class="week-number">4</div>
|
||||
<div class="month-day-cell today">
|
||||
<div class="month-day-number">20</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting">Status møde</div>
|
||||
<div class="month-event category-work">Refactoring</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">21</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-deadline">Beta release</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">22</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting">Architecture</div>
|
||||
<div class="month-event category-work">Performance</div>
|
||||
<div class="month-event category-deadline">Docs</div>
|
||||
<div class="month-event-more">+2</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">23</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work">Cleanup</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">24</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting">Weekly sync</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">25</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">26</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal">Fødselsdag</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Week 5 -->
|
||||
<div class="week-number">5</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">27</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting">Q1 planning</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">28</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work">Tech talks</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">29</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-deadline">Monthly report</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">30</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting">Team building</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">31</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-deadline">End of month</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">1</div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">2</div>
|
||||
</div>
|
||||
|
||||
<!-- Week 6 -->
|
||||
<div class="week-number">6</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">3</div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">4</div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">5</div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">6</div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">7</div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">8</div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">9</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add click handlers for events and days
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('month-event')) {
|
||||
console.log('Event clicked:', e.target.textContent);
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('month-event-more')) {
|
||||
console.log('Show more events');
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('month-day-cell') || e.target.closest('.month-day-cell')) {
|
||||
const cell = e.target.closest('.month-day-cell');
|
||||
const dayNumber = cell.querySelector('.month-day-number').textContent;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
848
.workbench/poc-layouts/month-view-expanded.html
Normal file
848
.workbench/poc-layouts/month-view-expanded.html
Normal file
|
|
@ -0,0 +1,848 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Calendar Plantempus - Month View Expanded</title>
|
||||
<style>
|
||||
/* Use existing Calendar Plantempus variables and styling */
|
||||
:root {
|
||||
/* Event duration sizing */
|
||||
--pixels-per-hour: 30px;
|
||||
--min-event-height: 15px; /* 30 min minimum */
|
||||
|
||||
--color-primary: #2196f3;
|
||||
--color-secondary: #ff9800;
|
||||
--color-success: #4caf50;
|
||||
--color-warning: #ff5722;
|
||||
--color-error: #f44336;
|
||||
--color-text: #2c3e50;
|
||||
--color-text-secondary: #6c757d;
|
||||
--color-border: #e0e0e0;
|
||||
--color-background: #ffffff;
|
||||
--color-surface: #f8f9fa;
|
||||
--color-hover: #f0f0f0;
|
||||
|
||||
--transition-fast: 0.15s ease;
|
||||
--border-radius: 4px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Month grid container */
|
||||
.month-container {
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
/* Month grid layout - rows auto-size based on content */
|
||||
.month-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 40px repeat(7, 1fr);
|
||||
grid-template-rows: 40px repeat(6, auto);
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
/* Week number header */
|
||||
.week-header {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
background: var(--color-surface);
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* Day headers */
|
||||
.month-day-header {
|
||||
background: var(--color-surface);
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 8px 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.month-day-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Week number cells */
|
||||
.week-number {
|
||||
grid-column: 1;
|
||||
background: var(--color-surface);
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
padding-top: 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Month day cells */
|
||||
.month-day-cell {
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 8px;
|
||||
background: var(--color-background);
|
||||
transition: background-color var(--transition-fast);
|
||||
min-height: 80px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.month-day-cell:hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
.month-day-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.month-day-cell.other-month {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.month-day-cell.today {
|
||||
background: #f0f8ff;
|
||||
border-left: 3px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.month-day-cell.weekend {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.month-day-number {
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.month-day-cell.today .month-day-number {
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Events container */
|
||||
.month-events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
/* Individual events with duration-based minimum height */
|
||||
.month-event {
|
||||
background: #e3f2fd;
|
||||
color: var(--color-text);
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
border-left: 3px solid var(--color-primary);
|
||||
/* Min-height set inline based on duration */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* Event time range */
|
||||
.event-time-range {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Event title */
|
||||
.event-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Event subtitle/description */
|
||||
.event-subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.month-event:hover {
|
||||
transform: translateX(2px);
|
||||
background: #d1e7fd;
|
||||
}
|
||||
|
||||
/* Event categories */
|
||||
.month-event.category-meeting {
|
||||
background: #e8f5e8;
|
||||
border-left-color: var(--color-success);
|
||||
}
|
||||
|
||||
.month-event.category-meeting:hover {
|
||||
background: #d4edd4;
|
||||
}
|
||||
|
||||
.month-event.category-deadline {
|
||||
background: #ffebee;
|
||||
border-left-color: var(--color-error);
|
||||
}
|
||||
|
||||
.month-event.category-deadline:hover {
|
||||
background: #ffd6dc;
|
||||
}
|
||||
|
||||
.month-event.category-work {
|
||||
background: #fff8e1;
|
||||
border-left-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.month-event.category-work:hover {
|
||||
background: #ffedcc;
|
||||
}
|
||||
|
||||
.month-event.category-personal {
|
||||
background: #f3e5f5;
|
||||
border-left-color: #9c27b0;
|
||||
}
|
||||
|
||||
.month-event.category-personal:hover {
|
||||
background: #e8d1ec;
|
||||
}
|
||||
|
||||
/* All-day events */
|
||||
.month-event.all-day {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.month-event.all-day .event-time-range,
|
||||
.month-event.all-day .event-title,
|
||||
.month-event.all-day .event-subtitle {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.month-event.all-day:hover {
|
||||
background: linear-gradient(135deg, #5a72e8 0%, #6b42a0 100%);
|
||||
}
|
||||
|
||||
/* Short events (30 min) - compact layout */
|
||||
.month-event.short-event {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.month-event.short-event .event-time-range {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.month-event.short-event .event-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.month-event.short-event .event-subtitle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.month-container {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.month-grid {
|
||||
grid-template-columns: 30px repeat(7, 1fr);
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.month-day-cell {
|
||||
padding: 4px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.month-day-header {
|
||||
padding: 8px 4px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.month-event {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.event-time-range,
|
||||
.event-subtitle {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.week-number {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
:root {
|
||||
--pixels-per-hour: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="month-container">
|
||||
<div class="month-grid">
|
||||
<!-- Week number header -->
|
||||
<div class="week-header">Uge</div>
|
||||
|
||||
<!-- Day headers -->
|
||||
<div class="month-day-header">Man</div>
|
||||
<div class="month-day-header">Tir</div>
|
||||
<div class="month-day-header">Ons</div>
|
||||
<div class="month-day-header">Tor</div>
|
||||
<div class="month-day-header">Fre</div>
|
||||
<div class="month-day-header">Lør</div>
|
||||
<div class="month-day-header">Søn</div>
|
||||
|
||||
<!-- Week 1 -->
|
||||
<div class="week-number">1</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">30</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">31</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal" style="min-height: 60px;">
|
||||
<div class="event-time-range">20:00 - 22:00</div>
|
||||
<div class="event-title">Nytårsaften</div>
|
||||
<div class="event-subtitle">Fest med familie og venner</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">1</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal all-day" style="min-height: 30px;">
|
||||
<div class="event-time-range">Hele dagen</div>
|
||||
<div class="event-title">Nytårsdag</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">2</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work" style="min-height: 30px;">
|
||||
<div class="event-time-range">09:00 - 10:00</div>
|
||||
<div class="event-title">Tilbage på arbejde</div>
|
||||
<div class="event-subtitle">Opstart efter ferien</div>
|
||||
</div>
|
||||
<div class="month-event category-meeting short-event" style="min-height: 15px;">
|
||||
<div class="event-time-range">10:00 - 10:30</div>
|
||||
<div class="event-title">Kick-off møde</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">3</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting" style="min-height: 45px;">
|
||||
<div class="event-time-range">09:00 - 10:30</div>
|
||||
<div class="event-title">Team møde</div>
|
||||
<div class="event-subtitle">Q1 planning og mål</div>
|
||||
</div>
|
||||
<div class="month-event category-work" style="min-height: 60px;">
|
||||
<div class="event-time-range">11:00 - 13:00</div>
|
||||
<div class="event-title">Projekt review</div>
|
||||
<div class="event-subtitle">Gennemgang af December projekter og status på Q1 initiativer</div>
|
||||
</div>
|
||||
<div class="month-event category-deadline short-event" style="min-height: 15px;">
|
||||
<div class="event-time-range">15:00 - 15:30</div>
|
||||
<div class="event-title">Sprint deadline</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">4</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">5</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal" style="min-height: 75px;">
|
||||
<div class="event-time-range">18:00 - 20:30</div>
|
||||
<div class="event-title">Familie middag</div>
|
||||
<div class="event-subtitle">Hos mormor og morfar</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Week 2 -->
|
||||
<div class="week-number">2</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">6</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting short-event" style="min-height: 7.5px;">
|
||||
<div class="event-time-range">09:15 - 09:30</div>
|
||||
<div class="event-title">Stand-up</div>
|
||||
</div>
|
||||
<div class="month-event category-work" style="min-height: 45px;">
|
||||
<div class="event-time-range">10:00 - 11:30</div>
|
||||
<div class="event-title">Code review</div>
|
||||
<div class="event-subtitle">Frontend refactoring PR</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">7</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-deadline" style="min-height: 30px;">
|
||||
<div class="event-time-range">12:00 - 13:00</div>
|
||||
<div class="event-title">Rapport deadline</div>
|
||||
<div class="event-subtitle">Månedlig status rapport</div>
|
||||
</div>
|
||||
<div class="month-event category-meeting" style="min-height: 30px;">
|
||||
<div class="event-time-range">14:00 - 15:00</div>
|
||||
<div class="event-title">1:1 med chef</div>
|
||||
<div class="event-subtitle">Karriere udvikling</div>
|
||||
</div>
|
||||
<div class="month-event category-work short-event" style="min-height: 15px;">
|
||||
<div class="event-time-range">15:30 - 16:00</div>
|
||||
<div class="event-title">Design review</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">8</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work" style="min-height: 30px;">
|
||||
<div class="event-time-range">10:00 - 11:00</div>
|
||||
<div class="event-title">Feature demo</div>
|
||||
<div class="event-subtitle">Ny dashboard funktionalitet</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">9</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting" style="min-height: 60px;">
|
||||
<div class="event-time-range">09:00 - 11:00</div>
|
||||
<div class="event-title">Planning møde</div>
|
||||
<div class="event-subtitle">Sprint 23 planning og estimering</div>
|
||||
</div>
|
||||
<div class="month-event category-work" style="min-height: 45px;">
|
||||
<div class="event-time-range">11:00 - 12:30</div>
|
||||
<div class="event-title">Design session</div>
|
||||
<div class="event-subtitle">UX workshop for ny feature</div>
|
||||
</div>
|
||||
<div class="month-event category-deadline short-event" style="min-height: 15px;">
|
||||
<div class="event-time-range">14:00 - 14:30</div>
|
||||
<div class="event-title">Release notes</div>
|
||||
</div>
|
||||
<div class="month-event category-meeting" style="min-height: 30px;">
|
||||
<div class="event-time-range">15:00 - 16:00</div>
|
||||
<div class="event-title">Tech sync</div>
|
||||
<div class="event-subtitle">Arkitektur diskussion</div>
|
||||
</div>
|
||||
<div class="month-event category-personal short-event" style="min-height: 15px;">
|
||||
<div class="event-time-range">17:00 - 17:30</div>
|
||||
<div class="event-title">Tandlæge</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">10</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work" style="min-height: 60px;">
|
||||
<div class="event-time-range">13:00 - 15:00</div>
|
||||
<div class="event-title">Sprint review</div>
|
||||
<div class="event-subtitle">Demo af Sprint 22 leverancer til stakeholders</div>
|
||||
</div>
|
||||
<div class="month-event category-meeting" style="min-height: 30px;">
|
||||
<div class="event-time-range">15:00 - 16:00</div>
|
||||
<div class="event-title">Retrospective</div>
|
||||
<div class="event-subtitle">Sprint 22 læringer</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">11</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">12</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal" style="min-height: 60px;">
|
||||
<div class="event-time-range">11:00 - 13:00</div>
|
||||
<div class="event-title">Brunch</div>
|
||||
<div class="event-subtitle">Med gamle venner fra uni</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Week 3 -->
|
||||
<div class="week-number">3</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">13</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting all-day" style="min-height: 30px;">
|
||||
<div class="event-time-range">Hele dagen</div>
|
||||
<div class="event-title">All hands møde</div>
|
||||
<div class="event-subtitle">Quarterly business update</div>
|
||||
</div>
|
||||
<div class="month-event category-work" style="min-height: 60px;">
|
||||
<div class="event-time-range">14:00 - 16:00</div>
|
||||
<div class="event-title">Architecture planning</div>
|
||||
<div class="event-subtitle">Microservices migration strategi</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">14</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work" style="min-height: 90px;">
|
||||
<div class="event-time-range">10:00 - 13:00</div>
|
||||
<div class="event-title">Feature deployment</div>
|
||||
<div class="event-subtitle">Production release og monitoring af ny payment feature</div>
|
||||
</div>
|
||||
<div class="month-event category-meeting" style="min-height: 45px;">
|
||||
<div class="event-time-range">15:00 - 16:30</div>
|
||||
<div class="event-title">Client call</div>
|
||||
<div class="event-subtitle">Requirements gathering</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">15</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-deadline" style="min-height: 60px;">
|
||||
<div class="event-time-range">10:00 - 12:00</div>
|
||||
<div class="event-title">Client presentation</div>
|
||||
<div class="event-subtitle">Q4 results og Q1 roadmap præsentation</div>
|
||||
</div>
|
||||
<div class="month-event category-meeting" style="min-height: 30px;">
|
||||
<div class="event-time-range">14:00 - 15:00</div>
|
||||
<div class="event-title">Team retrospective</div>
|
||||
<div class="event-subtitle">Monthly team health check</div>
|
||||
</div>
|
||||
<div class="month-event category-work" style="min-height: 30px;">
|
||||
<div class="event-time-range">16:00 - 17:00</div>
|
||||
<div class="event-title">Bug triage</div>
|
||||
<div class="event-subtitle">Priority 1 issues</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">16</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work" style="min-height: 45px;">
|
||||
<div class="event-time-range">11:00 - 12:30</div>
|
||||
<div class="event-title">Performance review</div>
|
||||
<div class="event-subtitle">Database optimization results</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">17</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting" style="min-height: 90px;">
|
||||
<div class="event-time-range">09:00 - 12:00</div>
|
||||
<div class="event-title">Sprint planning</div>
|
||||
<div class="event-subtitle">Sprint 24 - omfattende planning session med hele teamet</div>
|
||||
</div>
|
||||
<div class="month-event category-work" style="min-height: 60px;">
|
||||
<div class="event-time-range">13:00 - 15:00</div>
|
||||
<div class="event-title">Code cleanup</div>
|
||||
<div class="event-subtitle">Technical debt reduction</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">18</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal" style="min-height: 90px;">
|
||||
<div class="event-time-range">19:00 - 22:00</div>
|
||||
<div class="event-title">Koncert</div>
|
||||
<div class="event-subtitle">Royal Arena - med forband og afterparty</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">19</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
|
||||
<!-- Week 4 - Today -->
|
||||
<div class="week-number">4</div>
|
||||
<div class="month-day-cell today">
|
||||
<div class="month-day-number">20</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting short-event" style="min-height: 15px;">
|
||||
<div class="event-time-range">09:00 - 09:30</div>
|
||||
<div class="event-title">Status møde</div>
|
||||
</div>
|
||||
<div class="month-event category-work" style="min-height: 45px;">
|
||||
<div class="event-time-range">10:30 - 12:00</div>
|
||||
<div class="event-title">Refactoring session</div>
|
||||
<div class="event-subtitle">Legacy code modernization</div>
|
||||
</div>
|
||||
<div class="month-event category-deadline" style="min-height: 30px;">
|
||||
<div class="event-time-range">12:00 - 13:00</div>
|
||||
<div class="event-title">Documentation due</div>
|
||||
<div class="event-subtitle">API documentation update</div>
|
||||
</div>
|
||||
<div class="month-event category-meeting" style="min-height: 45px;">
|
||||
<div class="event-time-range">14:00 - 15:30</div>
|
||||
<div class="event-title">Architecture review</div>
|
||||
<div class="event-subtitle">System design review med senior architects</div>
|
||||
</div>
|
||||
<div class="month-event category-work" style="min-height: 30px;">
|
||||
<div class="event-time-range">15:30 - 16:30</div>
|
||||
<div class="event-title">Performance testing</div>
|
||||
<div class="event-subtitle">Load testing results</div>
|
||||
</div>
|
||||
<div class="month-event category-personal short-event" style="min-height: 15px;">
|
||||
<div class="event-time-range">17:00 - 17:30</div>
|
||||
<div class="event-title">Gym</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">21</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-deadline" style="min-height: 120px;">
|
||||
<div class="event-time-range">14:00 - 18:00</div>
|
||||
<div class="event-title">Beta release</div>
|
||||
<div class="event-subtitle">Full release process including deployment, smoke testing, monitoring setup og stakeholder notification</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">22</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting" style="min-height: 45px;">
|
||||
<div class="event-time-range">09:00 - 10:30</div>
|
||||
<div class="event-title">Architecture review</div>
|
||||
<div class="event-subtitle">Final design approval</div>
|
||||
</div>
|
||||
<div class="month-event category-work" style="min-height: 60px;">
|
||||
<div class="event-time-range">11:00 - 13:00</div>
|
||||
<div class="event-title">Performance testing</div>
|
||||
<div class="event-subtitle">Full regression test suite execution</div>
|
||||
</div>
|
||||
<div class="month-event category-deadline" style="min-height: 30px;">
|
||||
<div class="event-time-range">14:00 - 15:00</div>
|
||||
<div class="event-title">Documentation deadline</div>
|
||||
<div class="event-subtitle">User guide completion</div>
|
||||
</div>
|
||||
<div class="month-event category-meeting" style="min-height: 45px;">
|
||||
<div class="event-time-range">15:00 - 16:30</div>
|
||||
<div class="event-title">Stakeholder meeting</div>
|
||||
<div class="event-subtitle">Project status update</div>
|
||||
</div>
|
||||
<div class="month-event category-work short-event" style="min-height: 15px;">
|
||||
<div class="event-time-range">16:30 - 17:00</div>
|
||||
<div class="event-title">Deploy to staging</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">23</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-work all-day" style="min-height: 30px;">
|
||||
<div class="event-time-range">Hele dagen</div>
|
||||
<div class="event-title">Code cleanup day</div>
|
||||
<div class="event-subtitle">Team-wide technical debt reduction</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">24</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting" style="min-height: 30px;">
|
||||
<div class="event-time-range">14:00 - 15:00</div>
|
||||
<div class="event-title">Weekly sync</div>
|
||||
<div class="event-subtitle">Cross-team alignment</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">25</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
<div class="month-day-cell weekend">
|
||||
<div class="month-day-number">26</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-personal all-day" style="min-height: 30px;">
|
||||
<div class="event-time-range">Hele dagen</div>
|
||||
<div class="event-title">Fødselsdag</div>
|
||||
</div>
|
||||
<div class="month-event category-personal" style="min-height: 120px;">
|
||||
<div class="event-time-range">18:00 - 22:00</div>
|
||||
<div class="event-title">Fødselsdagsfest</div>
|
||||
<div class="event-subtitle">Stor fest med familie og venner, middag og underholdning</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Week 5 -->
|
||||
<div class="week-number">5</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">27</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting all-day" style="min-height: 30px;">
|
||||
<div class="event-time-range">Hele dagen</div>
|
||||
<div class="event-title">Q1 planning - Day 1</div>
|
||||
<div class="event-subtitle">Quarterly planning workshop</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">28</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting all-day" style="min-height: 30px;">
|
||||
<div class="event-time-range">Hele dagen</div>
|
||||
<div class="event-title">Q1 planning - Day 2</div>
|
||||
<div class="event-subtitle">OKR setting og roadmap</div>
|
||||
</div>
|
||||
<div class="month-event category-work" style="min-height: 60px;">
|
||||
<div class="event-time-range">14:00 - 16:00</div>
|
||||
<div class="event-title">Tech talks</div>
|
||||
<div class="event-subtitle">Knowledge sharing session om nye teknologier</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">29</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting all-day" style="min-height: 30px;">
|
||||
<div class="event-time-range">Hele dagen</div>
|
||||
<div class="event-title">Q1 planning - Day 3</div>
|
||||
<div class="event-subtitle">Final alignment og præsentation</div>
|
||||
</div>
|
||||
<div class="month-event category-deadline" style="min-height: 60px;">
|
||||
<div class="event-time-range">16:00 - 18:00</div>
|
||||
<div class="event-title">Monthly report</div>
|
||||
<div class="event-subtitle">Januar status rapport og metrics sammensætning</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">30</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-meeting" style="min-height: 90px;">
|
||||
<div class="event-time-range">15:00 - 18:00</div>
|
||||
<div class="event-title">Team building</div>
|
||||
<div class="event-subtitle">Off-site team building aktivitet med middag</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell">
|
||||
<div class="month-day-number">31</div>
|
||||
<div class="month-events">
|
||||
<div class="month-event category-deadline all-day" style="min-height: 30px;">
|
||||
<div class="event-time-range">Hele dagen</div>
|
||||
<div class="event-title">End of month</div>
|
||||
</div>
|
||||
<div class="month-event category-work" style="min-height: 60px;">
|
||||
<div class="event-time-range">14:00 - 16:00</div>
|
||||
<div class="event-title">Month wrap-up</div>
|
||||
<div class="event-subtitle">Januar review og Februar forberedelse</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">1</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">2</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
|
||||
<!-- Week 6 -->
|
||||
<div class="week-number">6</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">3</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">4</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">5</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">6</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">7</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">8</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
<div class="month-day-cell other-month">
|
||||
<div class="month-day-number">9</div>
|
||||
<div class="month-events"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Click handlers
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.month-event')) {
|
||||
const event = e.target.closest('.month-event');
|
||||
console.log('Event clicked:', event.querySelector('.event-title').textContent);
|
||||
|
||||
// Remove previous selection
|
||||
document.querySelectorAll('.month-event').forEach(el => {
|
||||
el.style.outline = '';
|
||||
});
|
||||
|
||||
// Highlight selected event
|
||||
event.style.outline = '2px solid var(--color-primary)';
|
||||
}
|
||||
|
||||
if (e.target.closest('.month-day-cell')) {
|
||||
const cell = e.target.closest('.month-day-cell');
|
||||
const dayNumber = cell.querySelector('.month-day-number').textContent;
|
||||
console.log('Day clicked:', dayNumber);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
De 6 vigtigste fund (med fixes)
|
||||
|
||||
Gruppering kan “brygge” mellem to grupper uden at merge dem
|
||||
groupEventsByStartTime finder første eksisterende gruppe med konflikt og lægger eventet deri. Hvis et nyt event konflikter med flere grupper, bliver grupperne ikke merged → inkonsistente “grid”-klumper. Løs: merge alle matchende grupper eller brug union-find/sweep-line konfliktsæt.
|
||||
|
||||
EventStackManager
|
||||
|
||||
ContainerType er “GRID” for alle grupper >1 — også ved dybe overlaps
|
||||
decideContainerType returnerer altid 'GRID' når events.length > 1. Det kan være tilsigtet, men så skal du være tryg ved, at lange overlappende events, der kun næsten starter samtidigt, stadig pakkes i kolonner fremfor “stacking”. Overvej: GRID kun når samtidighed er vigtigere end varighed, ellers fald tilbage til STACKING.
|
||||
|
||||
EventStackManager
|
||||
|
||||
Stack level-algoritmen kan eskalere niveauer unødigt
|
||||
createOptimizedStackLinks sætter stackLevel = max(overlappende tidligere) + 1. Det er mere “stak-tårn” end “før-ledig-kolonne” og giver højere niveauer end nødvendigt (ikke minimal farvelægning). Løs: interval partitioning med min-heap (giver laveste ledige level).
|
||||
|
||||
EventStackManager
|
||||
|
||||
Grid-top beregnes fra ét event, men børn positioneres relativt til containerStart
|
||||
I koordinatoren bruges earliestEvent til top, og renderer bruger earliestEvent.start som containerStart. Det er ok — men sørg for, at earliestEvent garanteret er det tidligste i gruppen og sortér eksplicit inden brug (robusthed mod fremtidige ændringer).
|
||||
|
||||
EventLayoutCoordinator
|
||||
|
||||
|
||||
|
||||
EventRenderer
|
||||
|
||||
Drag bruger rå new Date(...) i stedet for DateService
|
||||
Kan give TZ/DST-glitches. Brug samme parse/logik som resten.
|
||||
|
||||
EventRenderer
|
||||
|
||||
Ingen reflow af kolonne efter drop
|
||||
handleDragEnd normaliserer DOM men recalculerer ikke layout → forkert stacking/margin efter flyt. Kald din kolonne-pipeline igen for den berørte kolonne.
|
||||
|
||||
EventRenderer
|
||||
|
||||
Bonus: getEventsForColumn matcher kun start-dato === kolonnedato; events der krydser midnat forsvinder. Overvej interval-overlap mod døgnets [00:00–23:59:59.999].
|
||||
|
||||
EventRenderer
|
||||
|
||||
Målrettede patches (små og sikre)
|
||||
A) Merge grupper når et event rammer flere (EventStackManager)
|
||||
|
||||
Erstat den nuværende “find første gruppe”-logik med merge af alle matchende:
|
||||
|
||||
// inde i groupEventsByStartTime
|
||||
const matches: number[] = [];
|
||||
for (let gi = 0; gi < groups.length; gi++) {
|
||||
const group = groups[gi];
|
||||
const conflict = group.events.some(ge => {
|
||||
const s2s = Math.abs(event.start.getTime() - ge.start.getTime()) / 60000;
|
||||
if (s2s <= thresholdMinutes) return true;
|
||||
const e2s = (ge.end.getTime() - event.start.getTime()) / 60000;
|
||||
if (e2s > 0 && e2s <= thresholdMinutes) return true;
|
||||
const rev = (event.end.getTime() - ge.start.getTime()) / 60000;
|
||||
if (rev > 0 && rev <= thresholdMinutes) return true;
|
||||
return false;
|
||||
});
|
||||
if (conflict) matches.push(gi);
|
||||
}
|
||||
|
||||
if (matches.length === 0) {
|
||||
groups.push({ events: [event], containerType: 'NONE', startTime: event.start });
|
||||
} else {
|
||||
// merge alle matchende grupper + dette event
|
||||
const base = matches[0];
|
||||
groups[base].events.push(event);
|
||||
for (let i = matches.length - 1; i >= 1; i--) {
|
||||
const idx = matches[i];
|
||||
groups[base].events.push(...groups[idx].events);
|
||||
groups.splice(idx, 1);
|
||||
}
|
||||
// opdatér startTime til min start
|
||||
groups[base].startTime = new Date(
|
||||
Math.min(...groups[base].events.map(e => e.start.getTime()))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Nu undgår du “brobygning” der splitter reelt sammenhængende grupper.
|
||||
|
||||
EventStackManager
|
||||
|
||||
B) Minimal stack level med min-heap (EventStackManager)
|
||||
|
||||
Udskift level-tildeling med klassisk interval partitioning:
|
||||
|
||||
public createOptimizedStackLinks(events: CalendarEvent[]): Map<string, StackLink> {
|
||||
const res = new Map<string, StackLink>();
|
||||
if (!events.length) return res;
|
||||
|
||||
const sorted = [...events].sort((a,b)=> a.start.getTime() - b.start.getTime());
|
||||
type Col = { level: number; end: number };
|
||||
const cols: Col[] = []; // min-heap på end
|
||||
|
||||
const push = (c: Col) => { cols.push(c); cols.sort((x,y)=> x.end - y.end); };
|
||||
|
||||
for (const ev of sorted) {
|
||||
const t = ev.start.getTime();
|
||||
// find første kolonne der er fri
|
||||
let placed = false;
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
if (cols[i].end <= t) { cols[i].end = ev.end.getTime(); res.set(ev.id, { stackLevel: cols[i].level }); placed = true; break; }
|
||||
}
|
||||
if (!placed) { const level = cols.length; push({ level, end: ev.end.getTime() }); res.set(ev.id, { stackLevel: level }); }
|
||||
}
|
||||
|
||||
// evt. byg prev/next separat hvis nødvendigt
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
Dette giver laveste ledige niveau og undgår “trappetårne”.
|
||||
|
||||
EventStackManager
|
||||
|
||||
C) Konsolidér margin/zIndex + brug DateService i drag (EventRenderer)
|
||||
|
||||
Lad StackManager styre marginLeft konsekvent (og undgå magic numbers):
|
||||
|
||||
// renderGridGroup
|
||||
groupElement.style.top = `${gridGroup.position.top}px`;
|
||||
this.stackManager.applyVisualStyling(groupElement, gridGroup.stackLevel); // i stedet for *15
|
||||
this.stackManager.applyStackLinkToElement(groupElement, { stackLevel: gridGroup.stackLevel });
|
||||
|
||||
|
||||
EventRenderer
|
||||
|
||||
Brug DateService i drag:
|
||||
|
||||
public handleDragMove(payload: DragMoveEventPayload): void {
|
||||
if (!this.draggedClone || !payload.columnBounds) return;
|
||||
const swp = this.draggedClone as SwpEventElement;
|
||||
const colDate = this.dateService.parseISODate?.(payload.columnBounds.date) ?? new Date(payload.columnBounds.date);
|
||||
swp.updatePosition(colDate, payload.snappedY);
|
||||
}
|
||||
|
||||
public handleColumnChange(e: DragColumnChangeEventPayload): void {
|
||||
if (!this.draggedClone) return;
|
||||
const layer = e.newColumn.element.querySelector('swp-events-layer');
|
||||
if (layer && this.draggedClone.parentElement !== layer) {
|
||||
layer.appendChild(this.draggedClone);
|
||||
const currentTop = parseFloat(this.draggedClone.style.top) || 0;
|
||||
const swp = this.draggedClone as SwpEventElement;
|
||||
const colDate = this.dateService.parseISODate?.(e.newColumn.date) ?? new Date(e.newColumn.date);
|
||||
swp.updatePosition(colDate, currentTop);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
EventRenderer
|
||||
|
||||
D) Reflow efter drop (EventRenderer)
|
||||
|
||||
Genberegn layout for den berørte kolonne:
|
||||
|
||||
public handleDragEnd(id: string, original: HTMLElement, clone: HTMLElement, finalColumn: ColumnBounds): void {
|
||||
if (!clone || !original) { console.warn('Missing clone/original'); return; }
|
||||
this.fadeOutAndRemove(original);
|
||||
const cid = clone.dataset.eventId;
|
||||
if (cid && cid.startsWith('clone-')) clone.dataset.eventId = cid.replace('clone-','');
|
||||
clone.classList.remove('dragging');
|
||||
|
||||
const layer = finalColumn.element.querySelector('swp-events-layer') as HTMLElement | null;
|
||||
if (layer) {
|
||||
// 1) Hent kolonnens events fra din model/state (inkl. opdateret event)
|
||||
const columnEvents: CalendarEvent[] = /* ... */;
|
||||
// 2) Ryd
|
||||
layer.querySelectorAll('swp-event, swp-event-group').forEach(el => el.remove());
|
||||
// 3) Render igen via layout
|
||||
this.renderColumnEvents(columnEvents, layer);
|
||||
}
|
||||
|
||||
this.draggedClone = null;
|
||||
this.originalEvent = null;
|
||||
}
|
||||
|
||||
|
||||
EventRenderer
|
||||
|
||||
E) Døgn-overlap i kolonnefilter (EventRenderer)
|
||||
|
||||
Hvis ønsket (ellers behold din nuværende):
|
||||
|
||||
protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] {
|
||||
const d = column.dataset.date; if (!d) return [];
|
||||
const start = this.dateService.parseISODate(`${d}T00:00:00`);
|
||||
const end = this.dateService.parseISODate(`${d}T23:59:59.999`);
|
||||
return events.filter(ev => ev.start < end && ev.end > start);
|
||||
}
|
||||
|
||||
|
||||
EventRenderer
|
||||
|
||||
F) Eksplicit “earliest” i GRID (Coordinator)
|
||||
|
||||
Gør det robust i tilfælde af usorteret input:
|
||||
|
||||
const earliestEvent = [...gridCandidates].sort((a,b)=> a.start.getTime()-b.start.getTime())[0];
|
||||
const pos = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
|
||||
|
||||
|
||||
EventLayoutCoordinator
|
||||
|
||||
Mini-noter
|
||||
|
||||
allocateColumns er O(n²); det er fint for typiske dagvisninger. Hvis I ser >100 events/kolonne, kan I optimere med sweep-line + min-heap.
|
||||
|
||||
EventLayoutCoordinator
|
||||
|
||||
Overvej at lade koordinatoren returnere rene layout-maps (id → {level, z, margin}) og holde DOM-påføring 100% i renderer — det gør DnD-”reflow” enklere at teste.
|
||||
|
||||
EventLayoutCoordinator
|
||||
|
||||
|
||||
|
||||
EventRenderer
|
||||
62
.workbench/scenarios/scenario-1.html
Normal file
62
.workbench/scenarios/scenario-1.html
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 1: No Overlap</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 1: No Overlap</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Three sequential events with no time overlap. All events should have stack level 0 since they don't conflict.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=0 (stacked)<br>
|
||||
Event C: stackLevel=0 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S1A" data-title="Scenario 1: Event A" data-start="2025-10-06T08:00:00.000Z" data-end="2025-10-06T09:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 1px; height: 77px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 1: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S1B" data-title="Scenario 1: Event B" data-start="2025-10-06T09:00:00.000Z" data-end="2025-10-06T10:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 81px; height: 77px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="60">11:00 - 12:00</swp-event-time>
|
||||
<swp-event-title>Scenario 1: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S1C" data-title="Scenario 1: Event C" data-start="2025-10-06T10:00:00.000Z" data-end="2025-10-06T11:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 161px; height: 77px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="60">12:00 - 13:00</swp-event-time>
|
||||
<swp-event-title>Scenario 1: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-1',
|
||||
expected: [
|
||||
{ eventId: 'S1A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S1B', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S1C', stackLevel: 0, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
77
.workbench/scenarios/scenario-10.html
Normal file
77
.workbench/scenarios/scenario-10.html
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 10: Four Column Grid</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 10: Four Column Grid</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Four events all starting at exactly the same time (14:00). Tests maximum column sharing with a 4-column grid layout.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Grid group with 4 columns at stackLevel=0<br>
|
||||
Event A: in grid<br>
|
||||
Event B: in grid<br>
|
||||
Event C: in grid<br>
|
||||
Event D: in grid
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event-group class="cols-4 stack-level-0" data-stack-link="{"stackLevel":0}" style="top: 561px; margin-left: 0px; z-index: 100;">
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S10A" data-title="Scenario 10: Event A" data-start="2025-10-10T12:00:00.000Z" data-end="2025-10-10T13:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">14:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 10: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S10B" data-title="Scenario 10: Event B" data-start="2025-10-10T12:00:00.000Z" data-end="2025-10-10T13:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">14:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 10: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S10C" data-title="Scenario 10: Event C" data-start="2025-10-10T12:00:00.000Z" data-end="2025-10-10T13:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">14:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 10: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S10D" data-title="Scenario 10: Event D" data-start="2025-10-10T12:00:00.000Z" data-end="2025-10-10T13:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">14:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 10: Event D</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</swp-event-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-10',
|
||||
expected: [
|
||||
{ eventId: 'S10A', stackLevel: 0, cols: 4, type: 'grid' },
|
||||
{ eventId: 'S10B', stackLevel: 0, cols: 4, type: 'grid' },
|
||||
{ eventId: 'S10C', stackLevel: 0, cols: 4, type: 'grid' },
|
||||
{ eventId: 'S10D', stackLevel: 0, cols: 4, type: 'grid' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
61
.workbench/scenarios/scenario-2.html
Normal file
61
.workbench/scenarios/scenario-2.html
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 2: Column Sharing (Grid)</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 2: Column Sharing (Grid)</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Two events starting at exactly the same time (10:00). These should be placed in a grid container with 2 columns, allowing them to share horizontal space.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Grid group with 2 columns at stackLevel=0<br>
|
||||
Event A: in grid<br>
|
||||
Event B: in grid
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event-group class="cols-2 stack-level-0" data-stack-link="{"stackLevel":0}" style="top: 1px; margin-left: 0px; z-index: 100;">
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S2A" data-title="Scenario 2: Event A" data-start="2025-10-06T08:00:00.000Z" data-end="2025-10-06T09:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 2: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S2B" data-title="Scenario 2: Event B" data-start="2025-10-06T08:00:00.000Z" data-end="2025-10-06T09:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 2: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</swp-event-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-2',
|
||||
expected: [
|
||||
{ eventId: 'S2A', stackLevel: 0, cols: 2, type: 'grid' },
|
||||
{ eventId: 'S2B', stackLevel: 0, cols: 2, type: 'grid' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
69
.workbench/scenarios/scenario-3.html
Normal file
69
.workbench/scenarios/scenario-3.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 3: Nested Stacking</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 3: Nested Stacking</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Progressive nesting pattern: Event A (09:00-15:00) contains B (10:00-13:00), B contains C (11:00-12:00), and C overlaps with D (12:30-13:30). Tests correct stack level calculation for nested events.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=1 (stacked)<br>
|
||||
Event C: stackLevel=2 (stacked)<br>
|
||||
Event D: stackLevel=2 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S3A" data-title="Scenario 3: Event A" data-start="2025-10-07T07:00:00.000Z" data-end="2025-10-07T13:00:00.000Z" data-type="work" data-duration="360" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 1px; height: 357px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="360">09:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S3B" data-title="Scenario 3: Event B" data-start="2025-10-07T08:00:00.000Z" data-end="2025-10-07T11:00:00.000Z" data-type="work" data-duration="180" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 81px; height: 217px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="180">10:00 - 13:00</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S3C" data-title="Scenario 3: Event C" data-start="2025-10-07T09:00:00.000Z" data-end="2025-10-07T10:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":2}" style="position: absolute; top: 161px; height: 77px; left: 2px; right: 2px; margin-left: 30px; z-index: 102;">
|
||||
<swp-event-time data-duration="60">11:00 - 12:00</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S3D" data-title="Scenario 3: Event D" data-start="2025-10-07T10:30:00.000Z" data-end="2025-10-07T11:30:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":2}" style="position: absolute; top: 241px; height: 77px; left: 2px; right: 2px; margin-left: 30px; z-index: 102;">
|
||||
<swp-event-time data-duration="60">12:30 - 13:30</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event D</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-3',
|
||||
expected: [
|
||||
{ eventId: 'S3A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S3B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S3C', stackLevel: 2, type: 'stacked' },
|
||||
{ eventId: 'S3D', stackLevel: 2, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
69
.workbench/scenarios/scenario-4.html
Normal file
69
.workbench/scenarios/scenario-4.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 4: Complex Stacking</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 4: Complex Stacking</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Long event A (14:00-20:00) with multiple shorter events (B, C, D) nested inside at different times. Tests multiple stack levels with varying overlap patterns.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=1 (stacked)<br>
|
||||
Event C: stackLevel=2 (stacked)<br>
|
||||
Event D: stackLevel=1 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S4A" data-title="Scenario 4: Event A" data-start="2025-10-07T12:00:00.000Z" data-end="2025-10-07T18:00:00.000Z" data-type="work" data-duration="360" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 481px; height: 357px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="360">14:00 - 20:00</swp-event-time>
|
||||
<swp-event-title>Scenario 4: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S4B" data-title="Scenario 4: Event B" data-start="2025-10-07T13:00:00.000Z" data-end="2025-10-07T15:00:00.000Z" data-type="work" data-duration="120" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 561px; height: 157px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="120">15:00 - 17:00</swp-event-time>
|
||||
<swp-event-title>Scenario 4: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S4C" data-title="Scenario 4: Event C" data-start="2025-10-07T13:30:00.000Z" data-end="2025-10-07T14:30:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":2}" style="position: absolute; top: 601px; height: 77px; left: 2px; right: 2px; margin-left: 30px; z-index: 102;">
|
||||
<swp-event-time data-duration="60">15:30 - 16:30</swp-event-time>
|
||||
<swp-event-title>Scenario 4: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S4D" data-title="Scenario 4: Event D" data-start="2025-10-07T16:00:00.000Z" data-end="2025-10-07T17:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 721px; height: 77px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="60">18:00 - 19:00</swp-event-time>
|
||||
<swp-event-title>Scenario 4: Event D</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-4',
|
||||
expected: [
|
||||
{ eventId: 'S4A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S4B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S4C', stackLevel: 2, type: 'stacked' },
|
||||
{ eventId: 'S4D', stackLevel: 1, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
69
.workbench/scenarios/scenario-5.html
Normal file
69
.workbench/scenarios/scenario-5.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 5: Three Column Share</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 5: Three Column Share</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Three events all starting at exactly the same time (10:00). Should create a grid layout with 3 columns.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Grid group with 3 columns at stackLevel=0<br>
|
||||
Event A: in grid<br>
|
||||
Event B: in grid<br>
|
||||
Event C: in grid
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event-group class="cols-3 stack-level-0" data-stack-link="{"stackLevel":0}" style="top: 1px; margin-left: 0px; z-index: 100;">
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S5A" data-title="Scenario 5: Event A" data-start="2025-10-08T08:00:00.000Z" data-end="2025-10-08T09:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 5: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S5B" data-title="Scenario 5: Event B" data-start="2025-10-08T08:00:00.000Z" data-end="2025-10-08T09:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 5: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S5C" data-title="Scenario 5: Event C" data-start="2025-10-08T08:00:00.000Z" data-end="2025-10-08T09:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 5: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</swp-event-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-5',
|
||||
expected: [
|
||||
{ eventId: 'S5A', stackLevel: 0, cols: 3, type: 'grid' },
|
||||
{ eventId: 'S5B', stackLevel: 0, cols: 3, type: 'grid' },
|
||||
{ eventId: 'S5C', stackLevel: 0, cols: 3, type: 'grid' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
69
.workbench/scenarios/scenario-6.html
Normal file
69
.workbench/scenarios/scenario-6.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 6: Overlapping Pairs</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 6: Overlapping Pairs</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Two separate pairs of overlapping events: (A, B) and (C, D). Each pair should be independent with their own stack levels.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=1 (stacked)<br>
|
||||
Event C: stackLevel=0 (stacked)<br>
|
||||
Event D: stackLevel=1 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S6A" data-title="Scenario 6: Event A" data-start="2025-10-08T08:00:00.000Z" data-end="2025-10-08T10:00:00.000Z" data-type="work" data-duration="120" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 161px; height: 157px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="120">10:00 - 12:00</swp-event-time>
|
||||
<swp-event-title>Scenario 6: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S6B" data-title="Scenario 6: Event B" data-start="2025-10-08T09:00:00.000Z" data-end="2025-10-08T10:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 241px; height: 77px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="60">11:00 - 12:00</swp-event-time>
|
||||
<swp-event-title>Scenario 6: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S6C" data-title="Scenario 6: Event C" data-start="2025-10-08T11:00:00.000Z" data-end="2025-10-08T13:00:00.000Z" data-type="work" data-duration="120" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 401px; height: 157px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="120">13:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 6: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S6D" data-title="Scenario 6: Event D" data-start="2025-10-08T12:00:00.000Z" data-end="2025-10-08T13:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 481px; height: 77px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="60">14:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 6: Event D</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-6',
|
||||
expected: [
|
||||
{ eventId: 'S6A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S6B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S6C', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S6D', stackLevel: 1, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
62
.workbench/scenarios/scenario-7.html
Normal file
62
.workbench/scenarios/scenario-7.html
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 7: Long Event Container</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 7: Long Event Container</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>One long event (A: 09:00-15:00) containing two shorter events (B: 10:00-11:00, C: 12:00-13:00) that don't overlap with each other. B and C should both have the same stack level.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=1 (stacked)<br>
|
||||
Event C: stackLevel=1 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S7A" data-title="Scenario 7: Event A" data-start="2025-10-09T07:00:00.000Z" data-end="2025-10-09T13:00:00.000Z" data-type="work" data-duration="360" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 1px; height: 357px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="360">09:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 7: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S7B" data-title="Scenario 7: Event B" data-start="2025-10-09T08:00:00.000Z" data-end="2025-10-09T09:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 81px; height: 77px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 7: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S7C" data-title="Scenario 7: Event C" data-start="2025-10-09T10:00:00.000Z" data-end="2025-10-09T11:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 241px; height: 77px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="60">12:00 - 13:00</swp-event-time>
|
||||
<swp-event-title>Scenario 7: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-7',
|
||||
expected: [
|
||||
{ eventId: 'S7A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S7B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S7C', stackLevel: 1, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
55
.workbench/scenarios/scenario-8.html
Normal file
55
.workbench/scenarios/scenario-8.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 8: Edge-Adjacent Events</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 8: Edge-Adjacent Events</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Events that touch but don't overlap: Event A (10:00-11:00) and Event B (11:00-12:00). A ends exactly when B starts, so they should NOT stack.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=0 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S8A" data-title="Scenario 8: Event A" data-start="2025-10-09T08:00:00.000Z" data-end="2025-10-09T09:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 241px; height: 77px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 8: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S8B" data-title="Scenario 8: Event B" data-start="2025-10-09T09:00:00.000Z" data-end="2025-10-09T10:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 321px; height: 77px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="60">11:00 - 12:00</swp-event-time>
|
||||
<swp-event-title>Scenario 8: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-8',
|
||||
expected: [
|
||||
{ eventId: 'S8A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S8B', stackLevel: 0, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
67
.workbench/scenarios/scenario-9.html
Normal file
67
.workbench/scenarios/scenario-9.html
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 9: End-to-Start Chain</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 9: End-to-Start Chain</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Events linked by end-to-start conflicts within the threshold: Event A (12:00-13:00), Event B (12:30-13:30), and Event C (13:15-15:00). Even though C doesn't start close to A, it starts within threshold before B ends, creating a conflict chain A→B→C.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Grid group with 2 columns at stackLevel=0<br>
|
||||
Event A: in grid (column 1)<br>
|
||||
Event B: in grid (column 2)<br>
|
||||
Event C: in grid (column 1)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event-group class="cols-2 stack-level-0" data-stack-link="{"stackLevel":0}" style="top: 481px; margin-left: 0px; z-index: 100;">
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S9A" data-title="Scenario 9: Event A" data-start="2025-10-09T10:00:00.000Z" data-end="2025-10-09T11:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">12:00 - 13:00</swp-event-time>
|
||||
<swp-event-title>Scenario 9: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
<swp-event data-event-id="S9C" data-title="Scenario 9: Event C" data-start="2025-10-09T11:15:00.000Z" data-end="2025-10-09T13:00:00.000Z" data-type="work" data-duration="105" style="position: absolute; top: 100px; height: 137px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="105">13:15 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 9: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S9B" data-title="Scenario 9: Event B" data-start="2025-10-09T10:30:00.000Z" data-end="2025-10-09T11:30:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 40px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">12:30 - 13:30</swp-event-time>
|
||||
<swp-event-title>Scenario 9: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</swp-event-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-9',
|
||||
expected: [
|
||||
{ eventId: 'S9A', stackLevel: 0, cols: 2, type: 'grid' },
|
||||
{ eventId: 'S9B', stackLevel: 0, cols: 2, type: 'grid' },
|
||||
{ eventId: 'S9C', stackLevel: 0, cols: 2, type: 'grid' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
162
.workbench/scenarios/scenario-styles.css
Normal file
162
.workbench/scenarios/scenario-styles.css
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/* Shared styles for all scenario visualization files */
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.scenario-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.scenario-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.scenario-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.test-badge {
|
||||
display: inline-block;
|
||||
margin-left: 15px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.test-passed {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-failed {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-pending {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.scenario-description {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-left: 4px solid #b53f7a;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.expected-result {
|
||||
background: #e8f5e9;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.calendar-column {
|
||||
position: relative;
|
||||
width: 350px;
|
||||
height: 800px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* Event styling */
|
||||
swp-event-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
swp-event-group > div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
swp-event {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
border-left: 4px solid #b53f7a;
|
||||
background: #fff3e0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
swp-event-time {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
swp-event-title {
|
||||
display: block;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
color: #b53f7a;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.test-details {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.test-details h4 {
|
||||
margin-top: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.test-result-line {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.test-result-line.passed {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.test-result-line.failed {
|
||||
color: #f44336;
|
||||
}
|
||||
152
.workbench/scenarios/scenario-test-runner.js
Normal file
152
.workbench/scenarios/scenario-test-runner.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* Scenario Test Runner
|
||||
* Validates that rendered events match expected layout
|
||||
*/
|
||||
|
||||
export class ScenarioTestRunner {
|
||||
/**
|
||||
* Validate a scenario's rendered output
|
||||
* @param {string} scenarioId - e.g., "scenario-1"
|
||||
* @param {Array} expectedResults - Array of {eventId, stackLevel, cols?, type: 'grid'|'stacked'}
|
||||
* @returns {object} - {passed: boolean, results: Array, message: string}
|
||||
*/
|
||||
static validateScenario(scenarioId, expectedResults) {
|
||||
const results = [];
|
||||
let allPassed = true;
|
||||
|
||||
for (const expected of expectedResults) {
|
||||
const result = this.validateEvent(expected.eventId, expected);
|
||||
results.push(result);
|
||||
if (!result.passed) {
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
passed: allPassed,
|
||||
results,
|
||||
message: allPassed ? 'All tests passed ✅' : 'Some tests failed ❌'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single event
|
||||
*/
|
||||
static validateEvent(eventId, expected) {
|
||||
const eventEl = document.querySelector(`swp-event[data-event-id="${eventId}"]`);
|
||||
|
||||
if (!eventEl) {
|
||||
return {
|
||||
passed: false,
|
||||
eventId,
|
||||
message: `Event ${eventId} not found in DOM`
|
||||
};
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
|
||||
// Check if in grid group
|
||||
const gridGroup = eventEl.closest('swp-event-group');
|
||||
|
||||
if (expected.type === 'grid') {
|
||||
if (!gridGroup) {
|
||||
errors.push(`Expected to be in grid group, but found as stacked event`);
|
||||
} else {
|
||||
// Validate grid group properties
|
||||
const groupStackLevel = this.getStackLevel(gridGroup);
|
||||
if (groupStackLevel !== expected.stackLevel) {
|
||||
errors.push(`Grid group stack level: expected ${expected.stackLevel}, got ${groupStackLevel}`);
|
||||
}
|
||||
|
||||
if (expected.cols) {
|
||||
const cols = this.getColumnCount(gridGroup);
|
||||
if (cols !== expected.cols) {
|
||||
errors.push(`Grid columns: expected ${expected.cols}, got ${cols}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (expected.type === 'stacked') {
|
||||
if (gridGroup) {
|
||||
errors.push(`Expected to be stacked, but found in grid group`);
|
||||
} else {
|
||||
// Validate stacked event properties
|
||||
const stackLevel = this.getStackLevel(eventEl);
|
||||
if (stackLevel !== expected.stackLevel) {
|
||||
errors.push(`Stack level: expected ${expected.stackLevel}, got ${stackLevel}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
passed: errors.length === 0,
|
||||
eventId,
|
||||
message: errors.length === 0 ? '✅' : errors.join('; ')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stack level from element
|
||||
*/
|
||||
static getStackLevel(element) {
|
||||
const stackLink = element.getAttribute('data-stack-link');
|
||||
if (stackLink) {
|
||||
try {
|
||||
const parsed = JSON.parse(stackLink);
|
||||
return parsed.stackLevel;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Try class name fallback
|
||||
const classMatch = element.className.match(/stack-level-(\d+)/);
|
||||
return classMatch ? parseInt(classMatch[1]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column count from grid group
|
||||
*/
|
||||
static getColumnCount(gridGroup) {
|
||||
const classMatch = gridGroup.className.match(/cols-(\d+)/);
|
||||
return classMatch ? parseInt(classMatch[1]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display test results in the DOM
|
||||
*/
|
||||
static displayResults(containerId, results) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const badge = document.createElement('span');
|
||||
badge.className = `test-badge ${results.passed ? 'test-passed' : 'test-failed'}`;
|
||||
badge.textContent = results.message;
|
||||
container.appendChild(badge);
|
||||
|
||||
// Add detailed results
|
||||
const details = document.createElement('div');
|
||||
details.className = 'test-details';
|
||||
details.innerHTML = '<h4>Test Results:</h4>';
|
||||
|
||||
results.results.forEach(r => {
|
||||
const line = document.createElement('div');
|
||||
line.className = `test-result-line ${r.passed ? 'passed' : 'failed'}`;
|
||||
line.textContent = `${r.eventId}: ${r.message}`;
|
||||
details.appendChild(line);
|
||||
});
|
||||
|
||||
container.appendChild(details);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-run tests if window.scenarioTests is defined
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
if (window.scenarioTests) {
|
||||
const results = ScenarioTestRunner.validateScenario(
|
||||
window.scenarioTests.id,
|
||||
window.scenarioTests.expected
|
||||
);
|
||||
|
||||
ScenarioTestRunner.displayResults('test-results', results);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue