xref: /plugin/calendar/calendar-main.js (revision 9ccd446ecbe25932c2e89f7608c11495a1f1dbac)
11d05cddcSAtari911/**
21d05cddcSAtari911 * DokuWiki Compact Calendar Plugin JavaScript
31d05cddcSAtari911 * Loaded independently to avoid DokuWiki concatenation issues
41d05cddcSAtari911 */
51d05cddcSAtari911
61d05cddcSAtari911// Ensure DOKU_BASE is defined - check multiple sources
71d05cddcSAtari911if (typeof DOKU_BASE === 'undefined') {
81d05cddcSAtari911    // Try to get from global jsinfo object (DokuWiki standard)
91d05cddcSAtari911    if (typeof window.jsinfo !== 'undefined' && window.jsinfo.dokubase) {
101d05cddcSAtari911        window.DOKU_BASE = window.jsinfo.dokubase;
111d05cddcSAtari911    } else {
121d05cddcSAtari911        // Fallback: extract from script source path
131d05cddcSAtari911        var scripts = document.getElementsByTagName('script');
141d05cddcSAtari911        var pluginScriptPath = null;
151d05cddcSAtari911        for (var i = 0; i < scripts.length; i++) {
161d05cddcSAtari911            if (scripts[i].src && scripts[i].src.indexOf('calendar/script.js') !== -1) {
171d05cddcSAtari911                pluginScriptPath = scripts[i].src;
181d05cddcSAtari911                break;
191d05cddcSAtari911            }
201d05cddcSAtari911        }
211d05cddcSAtari911
221d05cddcSAtari911        if (pluginScriptPath) {
231d05cddcSAtari911            // Extract base path from: .../lib/plugins/calendar/script.js
241d05cddcSAtari911            var match = pluginScriptPath.match(/^(.*?)lib\/plugins\//);
251d05cddcSAtari911            window.DOKU_BASE = match ? match[1] : '/';
261d05cddcSAtari911        } else {
271d05cddcSAtari911            // Last resort: use root
281d05cddcSAtari911            window.DOKU_BASE = '/';
291d05cddcSAtari911        }
301d05cddcSAtari911    }
311d05cddcSAtari911}
321d05cddcSAtari911
331d05cddcSAtari911// Shorthand for convenience
341d05cddcSAtari911var DOKU_BASE = window.DOKU_BASE || '/';
351d05cddcSAtari911
36*9ccd446eSAtari911// Helper: propagate CSS variables from a calendar container to a target element
37*9ccd446eSAtari911// This is needed for dialogs/popups that use position:fixed (they inherit CSS vars
38*9ccd446eSAtari911// from DOM parents per spec, but some DokuWiki templates break this inheritance)
39*9ccd446eSAtari911function propagateThemeVars(calId, targetEl) {
40*9ccd446eSAtari911    if (!targetEl) return;
41*9ccd446eSAtari911    // Find the calendar container (could be cal_, panel_, sidebar-widget-, etc.)
42*9ccd446eSAtari911    const container = document.getElementById(calId)
43*9ccd446eSAtari911        || document.getElementById('sidebar-widget-' + calId)
44*9ccd446eSAtari911        || document.querySelector('[id$="' + calId + '"]');
45*9ccd446eSAtari911    if (!container) return;
46*9ccd446eSAtari911    const cs = getComputedStyle(container);
47*9ccd446eSAtari911    const vars = [
48*9ccd446eSAtari911        '--background-site', '--background-alt', '--background-header',
49*9ccd446eSAtari911        '--text-primary', '--text-bright', '--text-dim',
50*9ccd446eSAtari911        '--border-color', '--border-main',
51*9ccd446eSAtari911        '--cell-bg', '--cell-today-bg', '--grid-bg',
52*9ccd446eSAtari911        '--shadow-color', '--header-border', '--header-shadow',
53*9ccd446eSAtari911        '--btn-text'
54*9ccd446eSAtari911    ];
55*9ccd446eSAtari911    vars.forEach(v => {
56*9ccd446eSAtari911        const val = cs.getPropertyValue(v).trim();
57*9ccd446eSAtari911        if (val) targetEl.style.setProperty(v, val);
58*9ccd446eSAtari911    });
59*9ccd446eSAtari911}
60*9ccd446eSAtari911
611d05cddcSAtari911// Filter calendar by namespace
621d05cddcSAtari911window.filterCalendarByNamespace = function(calId, namespace) {
631d05cddcSAtari911    // Get current year and month from calendar
641d05cddcSAtari911    const container = document.getElementById(calId);
651d05cddcSAtari911    if (!container) {
661d05cddcSAtari911        console.error('Calendar container not found:', calId);
671d05cddcSAtari911        return;
681d05cddcSAtari911    }
691d05cddcSAtari911
701d05cddcSAtari911    const year = parseInt(container.dataset.year) || new Date().getFullYear();
711d05cddcSAtari911    const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1);
721d05cddcSAtari911
731d05cddcSAtari911    // Reload calendar with the filtered namespace
741d05cddcSAtari911    navCalendar(calId, year, month, namespace);
751d05cddcSAtari911};
761d05cddcSAtari911
771d05cddcSAtari911// Navigate to different month
781d05cddcSAtari911window.navCalendar = function(calId, year, month, namespace) {
791d05cddcSAtari911
801d05cddcSAtari911    const params = new URLSearchParams({
811d05cddcSAtari911        call: 'plugin_calendar',
821d05cddcSAtari911        action: 'load_month',
831d05cddcSAtari911        year: year,
841d05cddcSAtari911        month: month,
851d05cddcSAtari911        namespace: namespace,
861d05cddcSAtari911        _: new Date().getTime() // Cache buster
871d05cddcSAtari911    });
881d05cddcSAtari911
891d05cddcSAtari911    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
901d05cddcSAtari911        method: 'POST',
911d05cddcSAtari911        headers: {
921d05cddcSAtari911            'Content-Type': 'application/x-www-form-urlencoded',
931d05cddcSAtari911            'Cache-Control': 'no-cache, no-store, must-revalidate',
941d05cddcSAtari911            'Pragma': 'no-cache'
951d05cddcSAtari911        },
961d05cddcSAtari911        body: params.toString()
971d05cddcSAtari911    })
981d05cddcSAtari911    .then(r => r.json())
991d05cddcSAtari911    .then(data => {
1001d05cddcSAtari911        if (data.success) {
1011d05cddcSAtari911            rebuildCalendar(calId, data.year, data.month, data.events, namespace);
1021d05cddcSAtari911        } else {
1031d05cddcSAtari911            console.error('Failed to load month:', data.error);
1041d05cddcSAtari911        }
1051d05cddcSAtari911    })
1061d05cddcSAtari911    .catch(err => {
1071d05cddcSAtari911        console.error('Error loading month:', err);
1081d05cddcSAtari911    });
1091d05cddcSAtari911};
1101d05cddcSAtari911
1111d05cddcSAtari911// Jump to current month
1121d05cddcSAtari911window.jumpToToday = function(calId, namespace) {
1131d05cddcSAtari911    const today = new Date();
1141d05cddcSAtari911    const year = today.getFullYear();
1151d05cddcSAtari911    const month = today.getMonth() + 1; // JavaScript months are 0-indexed
1161d05cddcSAtari911    navCalendar(calId, year, month, namespace);
1171d05cddcSAtari911};
1181d05cddcSAtari911
1191d05cddcSAtari911// Jump to today for event panel
1201d05cddcSAtari911window.jumpTodayPanel = function(calId, namespace) {
1211d05cddcSAtari911    const today = new Date();
1221d05cddcSAtari911    const year = today.getFullYear();
1231d05cddcSAtari911    const month = today.getMonth() + 1;
1241d05cddcSAtari911    navEventPanel(calId, year, month, namespace);
1251d05cddcSAtari911};
1261d05cddcSAtari911
1271d05cddcSAtari911// Open month picker dialog
1281d05cddcSAtari911window.openMonthPicker = function(calId, currentYear, currentMonth, namespace) {
1291d05cddcSAtari911
1301d05cddcSAtari911    const overlay = document.getElementById('month-picker-overlay-' + calId);
1311d05cddcSAtari911
1321d05cddcSAtari911    const monthSelect = document.getElementById('month-picker-month-' + calId);
1331d05cddcSAtari911
1341d05cddcSAtari911    const yearSelect = document.getElementById('month-picker-year-' + calId);
1351d05cddcSAtari911
1361d05cddcSAtari911    if (!overlay) {
1371d05cddcSAtari911        console.error('Month picker overlay not found! ID:', 'month-picker-overlay-' + calId);
1381d05cddcSAtari911        return;
1391d05cddcSAtari911    }
1401d05cddcSAtari911
1411d05cddcSAtari911    if (!monthSelect || !yearSelect) {
1421d05cddcSAtari911        console.error('Select elements not found!');
1431d05cddcSAtari911        return;
1441d05cddcSAtari911    }
1451d05cddcSAtari911
1461d05cddcSAtari911    // Set current values
1471d05cddcSAtari911    monthSelect.value = currentMonth;
1481d05cddcSAtari911    yearSelect.value = currentYear;
1491d05cddcSAtari911
1501d05cddcSAtari911    // Show overlay
1511d05cddcSAtari911    overlay.style.display = 'flex';
1521d05cddcSAtari911};
1531d05cddcSAtari911
1541d05cddcSAtari911// Open month picker dialog for event panel
1551d05cddcSAtari911window.openMonthPickerPanel = function(calId, currentYear, currentMonth, namespace) {
1561d05cddcSAtari911    openMonthPicker(calId, currentYear, currentMonth, namespace);
1571d05cddcSAtari911};
1581d05cddcSAtari911
1591d05cddcSAtari911// Close month picker dialog
1601d05cddcSAtari911window.closeMonthPicker = function(calId) {
1611d05cddcSAtari911    const overlay = document.getElementById('month-picker-overlay-' + calId);
1621d05cddcSAtari911    overlay.style.display = 'none';
1631d05cddcSAtari911};
1641d05cddcSAtari911
1651d05cddcSAtari911// Jump to selected month
1661d05cddcSAtari911window.jumpToSelectedMonth = function(calId, namespace) {
1671d05cddcSAtari911    const monthSelect = document.getElementById('month-picker-month-' + calId);
1681d05cddcSAtari911    const yearSelect = document.getElementById('month-picker-year-' + calId);
1691d05cddcSAtari911
1701d05cddcSAtari911    const month = parseInt(monthSelect.value);
1711d05cddcSAtari911    const year = parseInt(yearSelect.value);
1721d05cddcSAtari911
1731d05cddcSAtari911    closeMonthPicker(calId);
1741d05cddcSAtari911
1751d05cddcSAtari911    // Check if this is a calendar or event panel
1761d05cddcSAtari911    const container = document.getElementById(calId);
1771d05cddcSAtari911    if (container && container.classList.contains('event-panel-standalone')) {
1781d05cddcSAtari911        navEventPanel(calId, year, month, namespace);
1791d05cddcSAtari911    } else {
1801d05cddcSAtari911        navCalendar(calId, year, month, namespace);
1811d05cddcSAtari911    }
1821d05cddcSAtari911};
1831d05cddcSAtari911
1841d05cddcSAtari911// Rebuild calendar grid after navigation
1851d05cddcSAtari911window.rebuildCalendar = function(calId, year, month, events, namespace) {
1861d05cddcSAtari911
1871d05cddcSAtari911    const container = document.getElementById(calId);
1881d05cddcSAtari911    const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
1891d05cddcSAtari911                       'July', 'August', 'September', 'October', 'November', 'December'];
1901d05cddcSAtari911
191*9ccd446eSAtari911    // Get theme data from container
192*9ccd446eSAtari911    const theme = container.dataset.theme || 'matrix';
193*9ccd446eSAtari911    let themeStyles = {};
194*9ccd446eSAtari911    try {
195*9ccd446eSAtari911        themeStyles = JSON.parse(container.dataset.themeStyles || '{}');
196*9ccd446eSAtari911    } catch (e) {
197*9ccd446eSAtari911        console.error('Failed to parse theme styles:', e);
198*9ccd446eSAtari911        themeStyles = {};
199*9ccd446eSAtari911    }
200*9ccd446eSAtari911
2011d05cddcSAtari911    // Preserve original namespace if not yet set
2021d05cddcSAtari911    if (!container.dataset.originalNamespace) {
2031d05cddcSAtari911        container.setAttribute('data-original-namespace', namespace || '');
2041d05cddcSAtari911    }
2051d05cddcSAtari911
2061d05cddcSAtari911    // Update container data attributes for current month/year
2071d05cddcSAtari911    container.setAttribute('data-year', year);
2081d05cddcSAtari911    container.setAttribute('data-month', month);
2091d05cddcSAtari911
2101d05cddcSAtari911    // Update embedded events data
2111d05cddcSAtari911    let eventsDataEl = document.getElementById('events-data-' + calId);
2121d05cddcSAtari911    if (eventsDataEl) {
2131d05cddcSAtari911        eventsDataEl.textContent = JSON.stringify(events);
2141d05cddcSAtari911    } else {
2151d05cddcSAtari911        eventsDataEl = document.createElement('script');
2161d05cddcSAtari911        eventsDataEl.type = 'application/json';
2171d05cddcSAtari911        eventsDataEl.id = 'events-data-' + calId;
2181d05cddcSAtari911        eventsDataEl.textContent = JSON.stringify(events);
2191d05cddcSAtari911        container.appendChild(eventsDataEl);
2201d05cddcSAtari911    }
2211d05cddcSAtari911
2221d05cddcSAtari911    // Update header
2231d05cddcSAtari911    const header = container.querySelector('.calendar-compact-header h3');
2241d05cddcSAtari911    header.textContent = monthNames[month - 1] + ' ' + year;
2251d05cddcSAtari911
2261d05cddcSAtari911    // Update or create namespace filter indicator
2271d05cddcSAtari911    let filterIndicator = container.querySelector('.calendar-namespace-filter');
2281d05cddcSAtari911    const shouldShowFilter = namespace && namespace !== '' && namespace !== '*' &&
2291d05cddcSAtari911                            namespace.indexOf('*') === -1 && namespace.indexOf(';') === -1;
2301d05cddcSAtari911
2311d05cddcSAtari911    if (shouldShowFilter) {
2321d05cddcSAtari911        // Show/update filter indicator
2331d05cddcSAtari911        if (!filterIndicator) {
2341d05cddcSAtari911            // Create filter indicator if it doesn't exist
2351d05cddcSAtari911            const headerDiv = container.querySelector('.calendar-compact-header');
236*9ccd446eSAtari911            if (headerDiv) {
2371d05cddcSAtari911                filterIndicator = document.createElement('div');
2381d05cddcSAtari911                filterIndicator.className = 'calendar-namespace-filter';
2391d05cddcSAtari911                filterIndicator.id = 'namespace-filter-' + calId;
2401d05cddcSAtari911                headerDiv.parentNode.insertBefore(filterIndicator, headerDiv.nextSibling);
2411d05cddcSAtari911            }
2421d05cddcSAtari911        }
2431d05cddcSAtari911
2441d05cddcSAtari911        if (filterIndicator) {
2451d05cddcSAtari911            filterIndicator.innerHTML =
2461d05cddcSAtari911                '<span class="namespace-filter-label">Filtering:</span>' +
2471d05cddcSAtari911                '<span class="namespace-filter-name">' + escapeHtml(namespace) + '</span>' +
2481d05cddcSAtari911                '<button class="namespace-filter-clear" onclick="clearNamespaceFilter(\'' + calId + '\')" title="Clear filter and show all namespaces">✕</button>';
2491d05cddcSAtari911            filterIndicator.style.display = 'flex';
2501d05cddcSAtari911        }
2511d05cddcSAtari911    } else {
2521d05cddcSAtari911        // Hide filter indicator
2531d05cddcSAtari911        if (filterIndicator) {
2541d05cddcSAtari911            filterIndicator.style.display = 'none';
2551d05cddcSAtari911        }
2561d05cddcSAtari911    }
2571d05cddcSAtari911
2581d05cddcSAtari911    // Update container's namespace attribute
2591d05cddcSAtari911    container.setAttribute('data-namespace', namespace || '');
2601d05cddcSAtari911
2611d05cddcSAtari911    // Update nav buttons
2621d05cddcSAtari911    let prevMonth = month - 1;
2631d05cddcSAtari911    let prevYear = year;
2641d05cddcSAtari911    if (prevMonth < 1) {
2651d05cddcSAtari911        prevMonth = 12;
2661d05cddcSAtari911        prevYear--;
2671d05cddcSAtari911    }
2681d05cddcSAtari911
2691d05cddcSAtari911    let nextMonth = month + 1;
2701d05cddcSAtari911    let nextYear = year;
2711d05cddcSAtari911    if (nextMonth > 12) {
2721d05cddcSAtari911        nextMonth = 1;
2731d05cddcSAtari911        nextYear++;
2741d05cddcSAtari911    }
2751d05cddcSAtari911
2761d05cddcSAtari911    const navBtns = container.querySelectorAll('.cal-nav-btn');
2771d05cddcSAtari911    navBtns[0].setAttribute('onclick', `navCalendar('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
2781d05cddcSAtari911    navBtns[1].setAttribute('onclick', `navCalendar('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
2791d05cddcSAtari911
2801d05cddcSAtari911    // Rebuild calendar grid
2811d05cddcSAtari911    const tbody = container.querySelector('.calendar-compact-grid tbody');
2821d05cddcSAtari911    const firstDay = new Date(year, month - 1, 1);
2831d05cddcSAtari911    const daysInMonth = new Date(year, month, 0).getDate();
2841d05cddcSAtari911    const dayOfWeek = firstDay.getDay();
2851d05cddcSAtari911
2861d05cddcSAtari911    // Calculate month boundaries
2871d05cddcSAtari911    const monthStart = new Date(year, month - 1, 1);
2881d05cddcSAtari911    const monthEnd = new Date(year, month - 1, daysInMonth);
2891d05cddcSAtari911
2901d05cddcSAtari911    // Build a map of all events with their date ranges
2911d05cddcSAtari911    const eventRanges = {};
2921d05cddcSAtari911    for (const [dateKey, dayEvents] of Object.entries(events)) {
2931d05cddcSAtari911        // Defensive check: ensure dayEvents is an array
2941d05cddcSAtari911        if (!Array.isArray(dayEvents)) {
2951d05cddcSAtari911            console.error('dayEvents is not an array for dateKey:', dateKey, 'value:', dayEvents);
2961d05cddcSAtari911            continue;
2971d05cddcSAtari911        }
2981d05cddcSAtari911
2991d05cddcSAtari911        // Only process events that could possibly overlap with this month/year
3001d05cddcSAtari911        const dateYear = parseInt(dateKey.split('-')[0]);
3011d05cddcSAtari911
3021d05cddcSAtari911        // Skip events from completely different years (unless they're very long multi-day events)
3031d05cddcSAtari911        if (Math.abs(dateYear - year) > 1) {
3041d05cddcSAtari911            continue;
3051d05cddcSAtari911        }
3061d05cddcSAtari911
3071d05cddcSAtari911        for (const evt of dayEvents) {
3081d05cddcSAtari911            const startDate = dateKey;
3091d05cddcSAtari911            const endDate = evt.endDate || dateKey;
3101d05cddcSAtari911
3111d05cddcSAtari911            // Check if event overlaps with current month
3121d05cddcSAtari911            const eventStart = new Date(startDate + 'T00:00:00');
3131d05cddcSAtari911            const eventEnd = new Date(endDate + 'T00:00:00');
3141d05cddcSAtari911
3151d05cddcSAtari911            // Skip if event doesn't overlap with current month
3161d05cddcSAtari911            if (eventEnd < monthStart || eventStart > monthEnd) {
3171d05cddcSAtari911                continue;
3181d05cddcSAtari911            }
3191d05cddcSAtari911
3201d05cddcSAtari911            // Create entry for each day the event spans
3211d05cddcSAtari911            const start = new Date(startDate + 'T00:00:00');
3221d05cddcSAtari911            const end = new Date(endDate + 'T00:00:00');
3231d05cddcSAtari911            const current = new Date(start);
3241d05cddcSAtari911
3251d05cddcSAtari911            while (current <= end) {
3261d05cddcSAtari911                const currentKey = current.toISOString().split('T')[0];
3271d05cddcSAtari911
3281d05cddcSAtari911                // Check if this date is in current month
3291d05cddcSAtari911                const currentDate = new Date(currentKey + 'T00:00:00');
3301d05cddcSAtari911                if (currentDate.getFullYear() === year && currentDate.getMonth() === month - 1) {
3311d05cddcSAtari911                    if (!eventRanges[currentKey]) {
3321d05cddcSAtari911                        eventRanges[currentKey] = [];
3331d05cddcSAtari911                    }
3341d05cddcSAtari911
3351d05cddcSAtari911                    // Add event with span information
3361d05cddcSAtari911                    const eventCopy = {...evt};
3371d05cddcSAtari911                    eventCopy._span_start = startDate;
3381d05cddcSAtari911                    eventCopy._span_end = endDate;
3391d05cddcSAtari911                    eventCopy._is_first_day = (currentKey === startDate);
3401d05cddcSAtari911                    eventCopy._is_last_day = (currentKey === endDate);
3411d05cddcSAtari911                    eventCopy._original_date = dateKey;
3421d05cddcSAtari911
3431d05cddcSAtari911                    // Check if event continues from previous month or to next month
3441d05cddcSAtari911                    eventCopy._continues_from_prev = (eventStart < monthStart);
3451d05cddcSAtari911                    eventCopy._continues_to_next = (eventEnd > monthEnd);
3461d05cddcSAtari911
3471d05cddcSAtari911                    eventRanges[currentKey].push(eventCopy);
3481d05cddcSAtari911                }
3491d05cddcSAtari911
3501d05cddcSAtari911                current.setDate(current.getDate() + 1);
3511d05cddcSAtari911            }
3521d05cddcSAtari911        }
3531d05cddcSAtari911    }
3541d05cddcSAtari911
3551d05cddcSAtari911    let html = '';
3561d05cddcSAtari911    let currentDay = 1;
3571d05cddcSAtari911    const rowCount = Math.ceil((daysInMonth + dayOfWeek) / 7);
3581d05cddcSAtari911
3591d05cddcSAtari911    for (let row = 0; row < rowCount; row++) {
3601d05cddcSAtari911        html += '<tr>';
3611d05cddcSAtari911        for (let col = 0; col < 7; col++) {
3621d05cddcSAtari911            if ((row === 0 && col < dayOfWeek) || currentDay > daysInMonth) {
363*9ccd446eSAtari911                html += `<td class="cal-empty"></td>`;
3641d05cddcSAtari911            } else {
3651d05cddcSAtari911                const dateKey = `${year}-${String(month).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`;
3661d05cddcSAtari911
3671d05cddcSAtari911                // Get today's date in local timezone
3681d05cddcSAtari911                const todayObj = new Date();
3691d05cddcSAtari911                const today = `${todayObj.getFullYear()}-${String(todayObj.getMonth() + 1).padStart(2, '0')}-${String(todayObj.getDate()).padStart(2, '0')}`;
3701d05cddcSAtari911
3711d05cddcSAtari911                const isToday = dateKey === today;
3721d05cddcSAtari911                const hasEvents = eventRanges[dateKey] && eventRanges[dateKey].length > 0;
3731d05cddcSAtari911
3741d05cddcSAtari911                let classes = 'cal-day';
3751d05cddcSAtari911                if (isToday) classes += ' cal-today';
3761d05cddcSAtari911                if (hasEvents) classes += ' cal-has-events';
3771d05cddcSAtari911
378*9ccd446eSAtari911                const dayNumClass = isToday ? 'day-num day-num-today' : 'day-num';
379*9ccd446eSAtari911
3801d05cddcSAtari911                html += `<td class="${classes}" data-date="${dateKey}" onclick="showDayPopup('${calId}', '${dateKey}', '${namespace}')">`;
381*9ccd446eSAtari911                html += `<span class="${dayNumClass}">${currentDay}</span>`;
3821d05cddcSAtari911
3831d05cddcSAtari911                if (hasEvents) {
3841d05cddcSAtari911                    // Sort events by time (no time first, then by time)
3851d05cddcSAtari911                    const sortedEvents = [...eventRanges[dateKey]].sort((a, b) => {
3861d05cddcSAtari911                        const timeA = a.time || '';
3871d05cddcSAtari911                        const timeB = b.time || '';
3881d05cddcSAtari911                        if (!timeA && timeB) return -1;
3891d05cddcSAtari911                        if (timeA && !timeB) return 1;
3901d05cddcSAtari911                        if (!timeA && !timeB) return 0;
3911d05cddcSAtari911                        return timeA.localeCompare(timeB);
3921d05cddcSAtari911                    });
3931d05cddcSAtari911
3941d05cddcSAtari911                    // Show colored stacked bars for each event
3951d05cddcSAtari911                    html += '<div class="event-indicators">';
3961d05cddcSAtari911                    for (const evt of sortedEvents) {
3971d05cddcSAtari911                        const eventId = evt.id || '';
3981d05cddcSAtari911                        const eventColor = evt.color || '#3498db';
3991d05cddcSAtari911                        const eventTitle = evt.title || 'Event';
400*9ccd446eSAtari911                        const eventTime = evt.time || '';
4011d05cddcSAtari911                        const originalDate = evt._original_date || dateKey;
4021d05cddcSAtari911                        const isFirstDay = evt._is_first_day !== undefined ? evt._is_first_day : true;
4031d05cddcSAtari911                        const isLastDay = evt._is_last_day !== undefined ? evt._is_last_day : true;
4041d05cddcSAtari911
4051d05cddcSAtari911                        let barClass = !eventTime ? 'event-bar-no-time' : 'event-bar-timed';
4061d05cddcSAtari911                        if (!isFirstDay) barClass += ' event-bar-continues';
4071d05cddcSAtari911                        if (!isLastDay) barClass += ' event-bar-continuing';
4081d05cddcSAtari911
4091d05cddcSAtari911                        html += `<span class="event-bar ${barClass}" `;
4101d05cddcSAtari911                        html += `style="background: ${eventColor};" `;
4111d05cddcSAtari911                        html += `title="${escapeHtml(eventTitle)}${eventTime ? ' @ ' + eventTime : ''}" `;
4121d05cddcSAtari911                        html += `onclick="event.stopPropagation(); highlightEvent('${calId}', '${eventId}', '${originalDate}');"></span>`;
4131d05cddcSAtari911                    }
4141d05cddcSAtari911                    html += '</div>';
4151d05cddcSAtari911                }
4161d05cddcSAtari911
4171d05cddcSAtari911                html += '</td>';
4181d05cddcSAtari911                currentDay++;
4191d05cddcSAtari911            }
4201d05cddcSAtari911        }
4211d05cddcSAtari911        html += '</tr>';
4221d05cddcSAtari911    }
4231d05cddcSAtari911
4241d05cddcSAtari911    tbody.innerHTML = html;
4251d05cddcSAtari911
4261d05cddcSAtari911    // Update Today button with current namespace
4271d05cddcSAtari911    const todayBtn = container.querySelector('.cal-today-btn');
4281d05cddcSAtari911    if (todayBtn) {
4291d05cddcSAtari911        todayBtn.setAttribute('onclick', `jumpToToday('${calId}', '${namespace}')`);
4301d05cddcSAtari911    }
4311d05cddcSAtari911
4321d05cddcSAtari911    // Update month picker with current namespace
4331d05cddcSAtari911    const monthPicker = container.querySelector('.calendar-month-picker');
4341d05cddcSAtari911    if (monthPicker) {
4351d05cddcSAtari911        monthPicker.setAttribute('onclick', `openMonthPicker('${calId}', ${year}, ${month}, '${namespace}')`);
4361d05cddcSAtari911    }
4371d05cddcSAtari911
4381d05cddcSAtari911    // Rebuild event list - server already filtered to current month
4391d05cddcSAtari911    const eventList = container.querySelector('.event-list-compact');
4401d05cddcSAtari911    eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month);
4411d05cddcSAtari911
4421d05cddcSAtari911    // Auto-scroll to first future event (past events will be above viewport)
4431d05cddcSAtari911    setTimeout(() => {
4441d05cddcSAtari911        const firstFuture = eventList.querySelector('[data-first-future="true"]');
4451d05cddcSAtari911        if (firstFuture) {
4461d05cddcSAtari911            firstFuture.scrollIntoView({ behavior: 'smooth', block: 'start' });
4471d05cddcSAtari911        }
4481d05cddcSAtari911    }, 100);
4491d05cddcSAtari911
4501d05cddcSAtari911    // Update title
4511d05cddcSAtari911    const title = container.querySelector('#eventlist-title-' + calId);
4521d05cddcSAtari911    title.textContent = 'Events';
4531d05cddcSAtari911};
4541d05cddcSAtari911
4551d05cddcSAtari911// Render event list from data
4561d05cddcSAtari911window.renderEventListFromData = function(events, calId, namespace, year, month) {
4571d05cddcSAtari911    if (!events || Object.keys(events).length === 0) {
4581d05cddcSAtari911        return '<p class="no-events-msg">No events this month</p>';
4591d05cddcSAtari911    }
4601d05cddcSAtari911
461*9ccd446eSAtari911    // Get theme data from container
462*9ccd446eSAtari911    const container = document.getElementById(calId);
463*9ccd446eSAtari911    let themeStyles = {};
464*9ccd446eSAtari911    if (container && container.dataset.themeStyles) {
465*9ccd446eSAtari911        try {
466*9ccd446eSAtari911            themeStyles = JSON.parse(container.dataset.themeStyles);
467*9ccd446eSAtari911        } catch (e) {
468*9ccd446eSAtari911            console.error('Failed to parse theme styles in renderEventListFromData:', e);
469*9ccd446eSAtari911        }
470*9ccd446eSAtari911    }
471*9ccd446eSAtari911
4721d05cddcSAtari911    // Check for time conflicts
4731d05cddcSAtari911    events = checkTimeConflicts(events, null);
4741d05cddcSAtari911
4751d05cddcSAtari911    let pastHtml = '';
4761d05cddcSAtari911    let futureHtml = '';
4771d05cddcSAtari911    let pastCount = 0;
4781d05cddcSAtari911
4791d05cddcSAtari911    const sortedDates = Object.keys(events).sort();
4801d05cddcSAtari911    const today = new Date();
4811d05cddcSAtari911    today.setHours(0, 0, 0, 0);
4821d05cddcSAtari911    const todayStr = today.toISOString().split('T')[0];
4831d05cddcSAtari911
4841d05cddcSAtari911    // Helper function to check if event is past (with 15-minute grace period)
4851d05cddcSAtari911    const isEventPast = function(dateKey, time) {
4861d05cddcSAtari911        // If event is on a past date, it's definitely past
4871d05cddcSAtari911        if (dateKey < todayStr) {
4881d05cddcSAtari911            return true;
4891d05cddcSAtari911        }
4901d05cddcSAtari911
4911d05cddcSAtari911        // If event is on a future date, it's definitely not past
4921d05cddcSAtari911        if (dateKey > todayStr) {
4931d05cddcSAtari911            return false;
4941d05cddcSAtari911        }
4951d05cddcSAtari911
4961d05cddcSAtari911        // Event is today - check time with grace period
4971d05cddcSAtari911        if (time && time.trim() !== '') {
4981d05cddcSAtari911            try {
4991d05cddcSAtari911                const now = new Date();
5001d05cddcSAtari911                const eventDateTime = new Date(dateKey + 'T' + time);
5011d05cddcSAtari911
5021d05cddcSAtari911                // Add 15-minute grace period
5031d05cddcSAtari911                const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000);
5041d05cddcSAtari911
5051d05cddcSAtari911                // Event is past if current time > event time + 15 minutes
5061d05cddcSAtari911                return now > gracePeriodEnd;
5071d05cddcSAtari911            } catch (e) {
5081d05cddcSAtari911                // If time parsing fails, treat as future
5091d05cddcSAtari911                return false;
5101d05cddcSAtari911            }
5111d05cddcSAtari911        }
5121d05cddcSAtari911
5131d05cddcSAtari911        // No time specified for today's event, treat as future
5141d05cddcSAtari911        return false;
5151d05cddcSAtari911    };
5161d05cddcSAtari911
5171d05cddcSAtari911    // Filter events to only current month if year/month provided
5181d05cddcSAtari911    const monthStart = year && month ? new Date(year, month - 1, 1) : null;
5191d05cddcSAtari911    const monthEnd = year && month ? new Date(year, month, 0, 23, 59, 59) : null;
5201d05cddcSAtari911
5211d05cddcSAtari911    for (const dateKey of sortedDates) {
5221d05cddcSAtari911        // Skip events not in current month if filtering
5231d05cddcSAtari911        if (monthStart && monthEnd) {
5241d05cddcSAtari911            const eventDate = new Date(dateKey + 'T00:00:00');
5251d05cddcSAtari911
5261d05cddcSAtari911            if (eventDate < monthStart || eventDate > monthEnd) {
5271d05cddcSAtari911                continue;
5281d05cddcSAtari911            }
5291d05cddcSAtari911        }
5301d05cddcSAtari911
5311d05cddcSAtari911        // Sort events within this day by time (all-day events at top)
5321d05cddcSAtari911        const dayEvents = events[dateKey];
5331d05cddcSAtari911        dayEvents.sort((a, b) => {
5341d05cddcSAtari911            const timeA = a.time && a.time.trim() !== '' ? a.time : null;
5351d05cddcSAtari911            const timeB = b.time && b.time.trim() !== '' ? b.time : null;
5361d05cddcSAtari911
5371d05cddcSAtari911            // All-day events (no time) go to the TOP
5381d05cddcSAtari911            if (timeA === null && timeB !== null) return -1; // A before B
5391d05cddcSAtari911            if (timeA !== null && timeB === null) return 1;  // A after B
5401d05cddcSAtari911            if (timeA === null && timeB === null) return 0;  // Both all-day, equal
5411d05cddcSAtari911
5421d05cddcSAtari911            // Both have times, sort chronologically
5431d05cddcSAtari911            return timeA.localeCompare(timeB);
5441d05cddcSAtari911        });
5451d05cddcSAtari911
5461d05cddcSAtari911        for (const event of dayEvents) {
5471d05cddcSAtari911            const isTask = event.isTask || false;
5481d05cddcSAtari911            const completed = event.completed || false;
5491d05cddcSAtari911
5501d05cddcSAtari911            // Use helper function to determine if event is past (with grace period)
5511d05cddcSAtari911            const isPast = isEventPast(dateKey, event.time);
5521d05cddcSAtari911            const isPastDue = isPast && isTask && !completed;
5531d05cddcSAtari911
5541d05cddcSAtari911            // Determine if this goes in past section
5551d05cddcSAtari911            const isPastOrCompleted = (isPast && (!isTask || completed)) || completed;
5561d05cddcSAtari911
5571d05cddcSAtari911            const eventHtml = renderEventItem(event, dateKey, calId, namespace);
5581d05cddcSAtari911
5591d05cddcSAtari911            if (isPastOrCompleted) {
5601d05cddcSAtari911                pastCount++;
5611d05cddcSAtari911                pastHtml += eventHtml;
5621d05cddcSAtari911            } else {
5631d05cddcSAtari911                futureHtml += eventHtml;
5641d05cddcSAtari911            }
5651d05cddcSAtari911        }
5661d05cddcSAtari911    }
5671d05cddcSAtari911
5681d05cddcSAtari911    let html = '';
5691d05cddcSAtari911
5701d05cddcSAtari911    // Add collapsible past events section if any exist
5711d05cddcSAtari911    if (pastCount > 0) {
5721d05cddcSAtari911        html += '<div class="past-events-section">';
5731d05cddcSAtari911        html += '<div class="past-events-toggle" onclick="togglePastEvents(\'' + calId + '\')">';
5741d05cddcSAtari911        html += '<span class="past-events-arrow" id="past-arrow-' + calId + '">▶</span> ';
5751d05cddcSAtari911        html += '<span class="past-events-label">Past Events (' + pastCount + ')</span>';
5761d05cddcSAtari911        html += '</div>';
5771d05cddcSAtari911        html += '<div class="past-events-content" id="past-events-' + calId + '" style="display:none;">';
5781d05cddcSAtari911        html += pastHtml;
5791d05cddcSAtari911        html += '</div>';
5801d05cddcSAtari911        html += '</div>';
5811d05cddcSAtari911    } else {
5821d05cddcSAtari911    }
5831d05cddcSAtari911
5841d05cddcSAtari911    // Add future events
5851d05cddcSAtari911    html += futureHtml;
5861d05cddcSAtari911
5871d05cddcSAtari911
5881d05cddcSAtari911    if (!html) {
5891d05cddcSAtari911        return '<p class="no-events-msg">No events this month</p>';
5901d05cddcSAtari911    }
5911d05cddcSAtari911
5921d05cddcSAtari911    return html;
5931d05cddcSAtari911};
5941d05cddcSAtari911
5951d05cddcSAtari911// Show day popup with events when clicking a date
5961d05cddcSAtari911window.showDayPopup = function(calId, date, namespace) {
5971d05cddcSAtari911    // Get events for this calendar
5981d05cddcSAtari911    const eventsDataEl = document.getElementById('events-data-' + calId);
5991d05cddcSAtari911    let events = {};
6001d05cddcSAtari911
6011d05cddcSAtari911    if (eventsDataEl) {
6021d05cddcSAtari911        try {
6031d05cddcSAtari911            events = JSON.parse(eventsDataEl.textContent);
6041d05cddcSAtari911        } catch (e) {
6051d05cddcSAtari911            console.error('Failed to parse events data:', e);
6061d05cddcSAtari911        }
6071d05cddcSAtari911    }
6081d05cddcSAtari911
6091d05cddcSAtari911    const dayEvents = events[date] || [];
6101d05cddcSAtari911
6111d05cddcSAtari911    // Check for conflicts on this day
6121d05cddcSAtari911    const dayEventsObj = {[date]: dayEvents};
6131d05cddcSAtari911    const checkedEvents = checkTimeConflicts(dayEventsObj, null);
6141d05cddcSAtari911    const dayEventsWithConflicts = checkedEvents[date] || dayEvents;
6151d05cddcSAtari911
6161d05cddcSAtari911    // Sort events: all-day at top, then chronological by time
6171d05cddcSAtari911    dayEventsWithConflicts.sort((a, b) => {
6181d05cddcSAtari911        const timeA = a.time && a.time.trim() !== '' ? a.time : null;
6191d05cddcSAtari911        const timeB = b.time && b.time.trim() !== '' ? b.time : null;
6201d05cddcSAtari911
6211d05cddcSAtari911        // All-day events (no time) go to the TOP
6221d05cddcSAtari911        if (timeA === null && timeB !== null) return -1; // A before B
6231d05cddcSAtari911        if (timeA !== null && timeB === null) return 1;  // A after B
6241d05cddcSAtari911        if (timeA === null && timeB === null) return 0;  // Both all-day, equal
6251d05cddcSAtari911
6261d05cddcSAtari911        // Both have times, sort chronologically
6271d05cddcSAtari911        return timeA.localeCompare(timeB);
6281d05cddcSAtari911    });
6291d05cddcSAtari911
6301d05cddcSAtari911    const dateObj = new Date(date + 'T00:00:00');
6311d05cddcSAtari911    const displayDate = dateObj.toLocaleDateString('en-US', {
6321d05cddcSAtari911        weekday: 'long',
6331d05cddcSAtari911        month: 'long',
6341d05cddcSAtari911        day: 'numeric',
6351d05cddcSAtari911        year: 'numeric'
6361d05cddcSAtari911    });
6371d05cddcSAtari911
6381d05cddcSAtari911    // Create popup
6391d05cddcSAtari911    let popup = document.getElementById('day-popup-' + calId);
6401d05cddcSAtari911    if (!popup) {
6411d05cddcSAtari911        popup = document.createElement('div');
6421d05cddcSAtari911        popup.id = 'day-popup-' + calId;
6431d05cddcSAtari911        popup.className = 'day-popup';
6441d05cddcSAtari911        document.body.appendChild(popup);
6451d05cddcSAtari911    }
6461d05cddcSAtari911
647*9ccd446eSAtari911    // Get theme styles
648*9ccd446eSAtari911    const container = document.getElementById(calId);
649*9ccd446eSAtari911    const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {};
650*9ccd446eSAtari911    const theme = container ? container.dataset.theme : 'matrix';
651*9ccd446eSAtari911
6521d05cddcSAtari911    let html = '<div class="day-popup-overlay" onclick="closeDayPopup(\'' + calId + '\')"></div>';
6531d05cddcSAtari911    html += '<div class="day-popup-content">';
6541d05cddcSAtari911    html += '<div class="day-popup-header">';
6551d05cddcSAtari911    html += '<h4>' + displayDate + '</h4>';
6561d05cddcSAtari911    html += '<button class="popup-close" onclick="closeDayPopup(\'' + calId + '\')">×</button>';
6571d05cddcSAtari911    html += '</div>';
6581d05cddcSAtari911
6591d05cddcSAtari911    html += '<div class="day-popup-body">';
6601d05cddcSAtari911
6611d05cddcSAtari911    if (dayEventsWithConflicts.length === 0) {
6621d05cddcSAtari911        html += '<p class="no-events-msg">No events on this day</p>';
6631d05cddcSAtari911    } else {
6641d05cddcSAtari911        html += '<div class="popup-events-list">';
6651d05cddcSAtari911        dayEventsWithConflicts.forEach(event => {
6661d05cddcSAtari911            const color = event.color || '#3498db';
6671d05cddcSAtari911
6681d05cddcSAtari911            // Use individual event namespace if available (for multi-namespace support)
6691d05cddcSAtari911            const eventNamespace = event._namespace !== undefined ? event._namespace : namespace;
6701d05cddcSAtari911
6711d05cddcSAtari911            // Check if this is a continuation (event started before this date)
6721d05cddcSAtari911            const originalStartDate = event.originalStartDate || event._dateKey || date;
6731d05cddcSAtari911            const isContinuation = originalStartDate < date;
6741d05cddcSAtari911
6751d05cddcSAtari911            // Convert to 12-hour format and handle time ranges
6761d05cddcSAtari911            let displayTime = '';
6771d05cddcSAtari911            if (event.time) {
6781d05cddcSAtari911                displayTime = formatTimeRange(event.time, event.endTime);
6791d05cddcSAtari911            }
6801d05cddcSAtari911
6811d05cddcSAtari911            // Multi-day indicator
6821d05cddcSAtari911            let multiDay = '';
6831d05cddcSAtari911            if (event.endDate && event.endDate !== date) {
6841d05cddcSAtari911                const endObj = new Date(event.endDate + 'T00:00:00');
6851d05cddcSAtari911                multiDay = ' → ' + endObj.toLocaleDateString('en-US', {
6861d05cddcSAtari911                    month: 'short',
6871d05cddcSAtari911                    day: 'numeric'
6881d05cddcSAtari911                });
6891d05cddcSAtari911            }
6901d05cddcSAtari911
6911d05cddcSAtari911            // Continuation message
6921d05cddcSAtari911            if (isContinuation) {
6931d05cddcSAtari911                const startObj = new Date(originalStartDate + 'T00:00:00');
6941d05cddcSAtari911                const startDisplay = startObj.toLocaleDateString('en-US', {
6951d05cddcSAtari911                    weekday: 'short',
6961d05cddcSAtari911                    month: 'short',
6971d05cddcSAtari911                    day: 'numeric'
6981d05cddcSAtari911                });
6991d05cddcSAtari911                html += '<div class="popup-continuation-notice">↪ Continues from ' + startDisplay + '</div>';
7001d05cddcSAtari911            }
7011d05cddcSAtari911
7021d05cddcSAtari911            html += '<div class="popup-event-item">';
7031d05cddcSAtari911            html += '<div class="event-color-bar" style="background: ' + color + ';"></div>';
7041d05cddcSAtari911            html += '<div class="popup-event-content">';
7051d05cddcSAtari911
7061d05cddcSAtari911            // Single line with title, time, date range, namespace, and actions
7071d05cddcSAtari911            html += '<div class="popup-event-main-row">';
7081d05cddcSAtari911            html += '<div class="popup-event-info-inline">';
7091d05cddcSAtari911            html += '<span class="popup-event-title">' + escapeHtml(event.title) + '</span>';
7101d05cddcSAtari911            if (displayTime) {
7111d05cddcSAtari911                html += '<span class="popup-event-time">�� ' + displayTime + '</span>';
7121d05cddcSAtari911            }
7131d05cddcSAtari911            if (multiDay) {
7141d05cddcSAtari911                html += '<span class="popup-event-multiday">' + multiDay + '</span>';
7151d05cddcSAtari911            }
7161d05cddcSAtari911            if (eventNamespace) {
7171d05cddcSAtari911                html += '<span class="popup-event-namespace">' + escapeHtml(eventNamespace) + '</span>';
7181d05cddcSAtari911            }
7191d05cddcSAtari911
7201d05cddcSAtari911            // Add conflict warning badge if event has conflicts
7211d05cddcSAtari911            if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) {
7221d05cddcSAtari911                // Build conflict list for tooltip
7231d05cddcSAtari911                let conflictList = [];
7241d05cddcSAtari911                event.conflictsWith.forEach(conflict => {
7251d05cddcSAtari911                    let conflictText = conflict.title;
7261d05cddcSAtari911                    if (conflict.time) {
7271d05cddcSAtari911                        conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')';
7281d05cddcSAtari911                    }
7291d05cddcSAtari911                    conflictList.push(conflictText);
7301d05cddcSAtari911                });
7311d05cddcSAtari911
732*9ccd446eSAtari911                html += '<span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>';
7331d05cddcSAtari911            }
7341d05cddcSAtari911
7351d05cddcSAtari911            html += '</div>';
7361d05cddcSAtari911            html += '<div class="popup-event-actions">';
7371d05cddcSAtari911            html += '<button class="event-edit-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">✏️</button>';
7381d05cddcSAtari911            html += '<button class="event-delete-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">��️</button>';
7391d05cddcSAtari911            html += '</div>';
7401d05cddcSAtari911            html += '</div>';
7411d05cddcSAtari911
7421d05cddcSAtari911            // Description on separate line if present
7431d05cddcSAtari911            if (event.description) {
7441d05cddcSAtari911                html += '<div class="popup-event-desc">' + renderDescription(event.description) + '</div>';
7451d05cddcSAtari911            }
7461d05cddcSAtari911
7471d05cddcSAtari911            html += '</div></div>';
7481d05cddcSAtari911        });
7491d05cddcSAtari911        html += '</div>';
7501d05cddcSAtari911    }
7511d05cddcSAtari911
7521d05cddcSAtari911    html += '</div>';
7531d05cddcSAtari911
7541d05cddcSAtari911    html += '<div class="day-popup-footer">';
7551d05cddcSAtari911    html += '<button class="btn-add-event" onclick="openAddEvent(\'' + calId + '\', \'' + namespace + '\', \'' + date + '\'); closeDayPopup(\'' + calId + '\')">+ Add Event</button>';
7561d05cddcSAtari911    html += '</div>';
7571d05cddcSAtari911
7581d05cddcSAtari911    html += '</div>';
7591d05cddcSAtari911
7601d05cddcSAtari911    popup.innerHTML = html;
7611d05cddcSAtari911    popup.style.display = 'flex';
762*9ccd446eSAtari911
763*9ccd446eSAtari911    // Propagate CSS vars from calendar container to popup (popup is outside container in DOM)
764*9ccd446eSAtari911    if (container) {
765*9ccd446eSAtari911        propagateThemeVars(calId, popup.querySelector('.day-popup-content'));
766*9ccd446eSAtari911    }
7671d05cddcSAtari911};
7681d05cddcSAtari911
7691d05cddcSAtari911// Close day popup
7701d05cddcSAtari911window.closeDayPopup = function(calId) {
7711d05cddcSAtari911    const popup = document.getElementById('day-popup-' + calId);
7721d05cddcSAtari911    if (popup) {
7731d05cddcSAtari911        popup.style.display = 'none';
7741d05cddcSAtari911    }
7751d05cddcSAtari911};
7761d05cddcSAtari911
7771d05cddcSAtari911// Show events for a specific day (for event list panel)
7781d05cddcSAtari911window.showDayEvents = function(calId, date, namespace) {
7791d05cddcSAtari911    const params = new URLSearchParams({
7801d05cddcSAtari911        call: 'plugin_calendar',
7811d05cddcSAtari911        action: 'load_month',
7821d05cddcSAtari911        year: date.split('-')[0],
7831d05cddcSAtari911        month: parseInt(date.split('-')[1]),
7841d05cddcSAtari911        namespace: namespace,
7851d05cddcSAtari911        _: new Date().getTime() // Cache buster
7861d05cddcSAtari911    });
7871d05cddcSAtari911
7881d05cddcSAtari911    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
7891d05cddcSAtari911        method: 'POST',
7901d05cddcSAtari911        headers: {
7911d05cddcSAtari911            'Content-Type': 'application/x-www-form-urlencoded',
7921d05cddcSAtari911            'Cache-Control': 'no-cache, no-store, must-revalidate',
7931d05cddcSAtari911            'Pragma': 'no-cache'
7941d05cddcSAtari911        },
7951d05cddcSAtari911        body: params.toString()
7961d05cddcSAtari911    })
7971d05cddcSAtari911    .then(r => r.json())
7981d05cddcSAtari911    .then(data => {
7991d05cddcSAtari911        if (data.success) {
8001d05cddcSAtari911            const eventList = document.getElementById('eventlist-' + calId);
8011d05cddcSAtari911            const events = data.events;
8021d05cddcSAtari911            const title = document.getElementById('eventlist-title-' + calId);
8031d05cddcSAtari911
8041d05cddcSAtari911            const dateObj = new Date(date + 'T00:00:00');
8051d05cddcSAtari911            const displayDate = dateObj.toLocaleDateString('en-US', {
8061d05cddcSAtari911                weekday: 'short',
8071d05cddcSAtari911                month: 'short',
8081d05cddcSAtari911                day: 'numeric'
8091d05cddcSAtari911            });
8101d05cddcSAtari911
8111d05cddcSAtari911            title.textContent = 'Events - ' + displayDate;
8121d05cddcSAtari911
8131d05cddcSAtari911            // Filter events for this day
8141d05cddcSAtari911            const dayEvents = events[date] || [];
8151d05cddcSAtari911
8161d05cddcSAtari911            if (dayEvents.length === 0) {
8171d05cddcSAtari911                eventList.innerHTML = '<p class="no-events-msg">No events on this day<br><button class="add-event-compact" onclick="openAddEvent(\'' + calId + '\', \'' + namespace + '\', \'' + date + '\')">+ Add Event</button></p>';
8181d05cddcSAtari911            } else {
8191d05cddcSAtari911                let html = '';
8201d05cddcSAtari911                dayEvents.forEach(event => {
8211d05cddcSAtari911                    html += renderEventItem(event, date, calId, namespace);
8221d05cddcSAtari911                });
8231d05cddcSAtari911                eventList.innerHTML = html;
8241d05cddcSAtari911            }
8251d05cddcSAtari911        }
8261d05cddcSAtari911    })
8271d05cddcSAtari911    .catch(err => console.error('Error:', err));
8281d05cddcSAtari911};
8291d05cddcSAtari911
8301d05cddcSAtari911// Render a single event item
8311d05cddcSAtari911window.renderEventItem = function(event, date, calId, namespace) {
832*9ccd446eSAtari911    // Get theme data from container
833*9ccd446eSAtari911    const container = document.getElementById(calId);
834*9ccd446eSAtari911    let themeStyles = {};
835*9ccd446eSAtari911    if (container && container.dataset.themeStyles) {
836*9ccd446eSAtari911        try {
837*9ccd446eSAtari911            themeStyles = JSON.parse(container.dataset.themeStyles);
838*9ccd446eSAtari911        } catch (e) {
839*9ccd446eSAtari911            console.error('Failed to parse theme styles:', e);
840*9ccd446eSAtari911        }
841*9ccd446eSAtari911    }
842*9ccd446eSAtari911
8431d05cddcSAtari911    // Check if this event is in the past or today (with 15-minute grace period)
8441d05cddcSAtari911    const today = new Date();
8451d05cddcSAtari911    today.setHours(0, 0, 0, 0);
8461d05cddcSAtari911    const todayStr = today.toISOString().split('T')[0];
8471d05cddcSAtari911    const eventDate = new Date(date + 'T00:00:00');
8481d05cddcSAtari911
8491d05cddcSAtari911    // Helper to determine if event is past with grace period
8501d05cddcSAtari911    let isPast;
8511d05cddcSAtari911    if (date < todayStr) {
8521d05cddcSAtari911        isPast = true; // Past date
8531d05cddcSAtari911    } else if (date > todayStr) {
8541d05cddcSAtari911        isPast = false; // Future date
8551d05cddcSAtari911    } else {
8561d05cddcSAtari911        // Today - check time with grace period
8571d05cddcSAtari911        if (event.time && event.time.trim() !== '') {
8581d05cddcSAtari911            try {
8591d05cddcSAtari911                const now = new Date();
8601d05cddcSAtari911                const eventDateTime = new Date(date + 'T' + event.time);
8611d05cddcSAtari911                const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000);
8621d05cddcSAtari911                isPast = now > gracePeriodEnd;
8631d05cddcSAtari911            } catch (e) {
8641d05cddcSAtari911                isPast = false;
8651d05cddcSAtari911            }
8661d05cddcSAtari911        } else {
8671d05cddcSAtari911            isPast = false; // No time, treat as future
8681d05cddcSAtari911        }
8691d05cddcSAtari911    }
8701d05cddcSAtari911
8711d05cddcSAtari911    const isToday = eventDate.getTime() === today.getTime();
8721d05cddcSAtari911
8731d05cddcSAtari911    // Format date display with day of week
8741d05cddcSAtari911    const displayDateKey = event.originalStartDate || date;
8751d05cddcSAtari911    const dateObj = new Date(displayDateKey + 'T00:00:00');
8761d05cddcSAtari911    const displayDate = dateObj.toLocaleDateString('en-US', {
8771d05cddcSAtari911        weekday: 'short',
8781d05cddcSAtari911        month: 'short',
8791d05cddcSAtari911        day: 'numeric'
8801d05cddcSAtari911    });
8811d05cddcSAtari911
8821d05cddcSAtari911    // Convert to 12-hour format and handle time ranges
8831d05cddcSAtari911    let displayTime = '';
8841d05cddcSAtari911    if (event.time) {
8851d05cddcSAtari911        displayTime = formatTimeRange(event.time, event.endTime);
8861d05cddcSAtari911    }
8871d05cddcSAtari911
8881d05cddcSAtari911    // Multi-day indicator
8891d05cddcSAtari911    let multiDay = '';
8901d05cddcSAtari911    if (event.endDate && event.endDate !== displayDateKey) {
8911d05cddcSAtari911        const endObj = new Date(event.endDate + 'T00:00:00');
8921d05cddcSAtari911        multiDay = ' → ' + endObj.toLocaleDateString('en-US', {
8931d05cddcSAtari911            weekday: 'short',
8941d05cddcSAtari911            month: 'short',
8951d05cddcSAtari911            day: 'numeric'
8961d05cddcSAtari911        });
8971d05cddcSAtari911    }
8981d05cddcSAtari911
8991d05cddcSAtari911    const completedClass = event.completed ? ' event-completed' : '';
9001d05cddcSAtari911    const isTask = event.isTask || false;
9011d05cddcSAtari911    const completed = event.completed || false;
9021d05cddcSAtari911    const isPastDue = isPast && isTask && !completed;
9031d05cddcSAtari911    const pastClass = (isPast && !isPastDue) ? ' event-past' : '';
9041d05cddcSAtari911    const pastDueClass = isPastDue ? ' event-pastdue' : '';
9051d05cddcSAtari911    const color = event.color || '#3498db';
9061d05cddcSAtari911
907*9ccd446eSAtari911    // Only inline style needed: border-left-color for event color indicator
9081d05cddcSAtari911    let html = '<div class="event-compact-item' + completedClass + pastClass + pastDueClass + '" data-event-id="' + event.id + '" data-date="' + date + '" style="border-left-color: ' + color + ';" onclick="' + (isPast && !isPastDue ? 'togglePastEventExpand(this)' : '') + '">';
9091d05cddcSAtari911
9101d05cddcSAtari911    html += '<div class="event-info">';
9111d05cddcSAtari911    html += '<div class="event-title-row">';
9121d05cddcSAtari911    html += '<span class="event-title-compact">' + escapeHtml(event.title) + '</span>';
9131d05cddcSAtari911    html += '</div>';
9141d05cddcSAtari911
9151d05cddcSAtari911    // Show meta and description for non-past events AND past due tasks
9161d05cddcSAtari911    if (!isPast || isPastDue) {
9171d05cddcSAtari911        html += '<div class="event-meta-compact">';
9181d05cddcSAtari911        html += '<span class="event-date-time">' + displayDate + multiDay;
9191d05cddcSAtari911        if (displayTime) {
9201d05cddcSAtari911            html += ' • ' + displayTime;
9211d05cddcSAtari911        }
9221d05cddcSAtari911        // Add PAST DUE or TODAY badge
9231d05cddcSAtari911        if (isPastDue) {
9241d05cddcSAtari911            html += ' <span class="event-pastdue-badge">PAST DUE</span>';
9251d05cddcSAtari911        } else if (isToday) {
9261d05cddcSAtari911            html += ' <span class="event-today-badge">TODAY</span>';
9271d05cddcSAtari911        }
928*9ccd446eSAtari911        // Add namespace badge
9291d05cddcSAtari911        let eventNamespace = event.namespace || '';
9301d05cddcSAtari911        if (!eventNamespace && event._namespace !== undefined) {
931*9ccd446eSAtari911            eventNamespace = event._namespace;
9321d05cddcSAtari911        }
9331d05cddcSAtari911        if (eventNamespace) {
934*9ccd446eSAtari911            html += ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' + calId + '\', \'' + escapeHtml(eventNamespace) + '\')" title="Click to filter by this namespace">' + escapeHtml(eventNamespace) + '</span>';
9351d05cddcSAtari911        }
9361d05cddcSAtari911        // Add conflict warning if event has time conflicts
9371d05cddcSAtari911        if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) {
9381d05cddcSAtari911            let conflictList = [];
9391d05cddcSAtari911            event.conflictsWith.forEach(conflict => {
9401d05cddcSAtari911                let conflictText = conflict.title;
9411d05cddcSAtari911                if (conflict.time) {
9421d05cddcSAtari911                    conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')';
9431d05cddcSAtari911                }
9441d05cddcSAtari911                conflictList.push(conflictText);
9451d05cddcSAtari911            });
9461d05cddcSAtari911
947*9ccd446eSAtari911            html += ' <span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>';
9481d05cddcSAtari911        }
9491d05cddcSAtari911        html += '</span>';
9501d05cddcSAtari911        html += '</div>';
9511d05cddcSAtari911
9521d05cddcSAtari911        if (event.description) {
9531d05cddcSAtari911            html += '<div class="event-desc-compact">' + renderDescription(event.description) + '</div>';
9541d05cddcSAtari911        }
9551d05cddcSAtari911    } else {
9561d05cddcSAtari911        // For past events (not past due), store data in hidden divs for expand/collapse
9571d05cddcSAtari911        html += '<div class="event-meta-compact" style="display: none;">';
9581d05cddcSAtari911        html += '<span class="event-date-time">' + displayDate + multiDay;
9591d05cddcSAtari911        if (displayTime) {
9601d05cddcSAtari911            html += ' • ' + displayTime;
9611d05cddcSAtari911        }
9621d05cddcSAtari911        // Add namespace badge for past events too
9631d05cddcSAtari911        let eventNamespace = event.namespace || '';
9641d05cddcSAtari911        if (!eventNamespace && event._namespace !== undefined) {
9651d05cddcSAtari911            eventNamespace = event._namespace;
9661d05cddcSAtari911        }
9671d05cddcSAtari911        if (eventNamespace) {
968*9ccd446eSAtari911            html += ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' + calId + '\', \'' + escapeHtml(eventNamespace) + '\')" title="Click to filter by this namespace">' + escapeHtml(eventNamespace) + '</span>';
969*9ccd446eSAtari911        }
970*9ccd446eSAtari911        // Add conflict warning for past events too
971*9ccd446eSAtari911        if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) {
972*9ccd446eSAtari911            let conflictList = [];
973*9ccd446eSAtari911            event.conflictsWith.forEach(conflict => {
974*9ccd446eSAtari911                let conflictText = conflict.title;
975*9ccd446eSAtari911                if (conflict.time) {
976*9ccd446eSAtari911                    conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')';
977*9ccd446eSAtari911                }
978*9ccd446eSAtari911                conflictList.push(conflictText);
979*9ccd446eSAtari911            });
980*9ccd446eSAtari911
981*9ccd446eSAtari911            html += ' <span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>';
9821d05cddcSAtari911        }
9831d05cddcSAtari911        html += '</span>';
9841d05cddcSAtari911        html += '</div>';
9851d05cddcSAtari911
9861d05cddcSAtari911        if (event.description) {
9871d05cddcSAtari911            html += '<div class="event-desc-compact" style="display: none;">' + renderDescription(event.description) + '</div>';
9881d05cddcSAtari911        }
9891d05cddcSAtari911    }
9901d05cddcSAtari911
9911d05cddcSAtari911    html += '</div>'; // event-info
9921d05cddcSAtari911
9931d05cddcSAtari911    // Use stored namespace from event, fallback to _namespace, then passed namespace
9941d05cddcSAtari911    let buttonNamespace = event.namespace || '';
9951d05cddcSAtari911    if (!buttonNamespace && event._namespace !== undefined) {
9961d05cddcSAtari911        buttonNamespace = event._namespace;
9971d05cddcSAtari911    }
9981d05cddcSAtari911    if (!buttonNamespace) {
9991d05cddcSAtari911        buttonNamespace = namespace;
10001d05cddcSAtari911    }
10011d05cddcSAtari911
10021d05cddcSAtari911    html += '<div class="event-actions-compact">';
10031d05cddcSAtari911    html += '<button class="event-action-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">��️</button>';
10041d05cddcSAtari911    html += '<button class="event-action-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">✏️</button>';
10051d05cddcSAtari911    html += '</div>';
10061d05cddcSAtari911
10071d05cddcSAtari911    // Checkbox for tasks - ON THE FAR RIGHT
10081d05cddcSAtari911    if (isTask) {
10091d05cddcSAtari911        const checked = completed ? 'checked' : '';
10101d05cddcSAtari911        html += '<input type="checkbox" class="task-checkbox" ' + checked + ' onclick="toggleTaskComplete(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\', this.checked)">';
10111d05cddcSAtari911    }
10121d05cddcSAtari911
10131d05cddcSAtari911    html += '</div>';
10141d05cddcSAtari911
10151d05cddcSAtari911    return html;
10161d05cddcSAtari911};
10171d05cddcSAtari911
10181d05cddcSAtari911// Render description with rich content support
10191d05cddcSAtari911window.renderDescription = function(description) {
10201d05cddcSAtari911    if (!description) return '';
10211d05cddcSAtari911
10221d05cddcSAtari911    // First, convert DokuWiki/Markdown syntax to placeholder tokens (before escaping)
10231d05cddcSAtari911    // Use a format that won't be affected by HTML escaping: \x00TOKEN_N\x00
10241d05cddcSAtari911
10251d05cddcSAtari911    let rendered = description;
10261d05cddcSAtari911    const tokens = [];
10271d05cddcSAtari911    let tokenIndex = 0;
10281d05cddcSAtari911
10291d05cddcSAtari911    // Convert DokuWiki image syntax {{image.jpg}} to tokens
10301d05cddcSAtari911    rendered = rendered.replace(/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/g, function(match, imagePath, alt) {
10311d05cddcSAtari911        imagePath = imagePath.trim();
10321d05cddcSAtari911        alt = alt ? alt.trim() : '';
10331d05cddcSAtari911
10341d05cddcSAtari911        let imageHtml;
10351d05cddcSAtari911        // Handle external URLs
10361d05cddcSAtari911        if (imagePath.match(/^https?:\/\//)) {
10371d05cddcSAtari911            imageHtml = '<img src="' + imagePath + '" alt="' + escapeHtml(alt) + '" class="event-image" />';
10381d05cddcSAtari911        } else {
10391d05cddcSAtari911            // Handle internal DokuWiki images
10401d05cddcSAtari911            const imageUrl = DOKU_BASE + 'lib/exe/fetch.php?media=' + encodeURIComponent(imagePath);
10411d05cddcSAtari911            imageHtml = '<img src="' + imageUrl + '" alt="' + escapeHtml(alt) + '" class="event-image" />';
10421d05cddcSAtari911        }
10431d05cddcSAtari911
10441d05cddcSAtari911        const token = '\x00TOKEN' + tokenIndex + '\x00';
10451d05cddcSAtari911        tokens[tokenIndex] = imageHtml;
10461d05cddcSAtari911        tokenIndex++;
10471d05cddcSAtari911        return token;
10481d05cddcSAtari911    });
10491d05cddcSAtari911
10501d05cddcSAtari911    // Convert DokuWiki link syntax [[link|text]] to tokens
10511d05cddcSAtari911    rendered = rendered.replace(/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/g, function(match, link, text) {
10521d05cddcSAtari911        link = link.trim();
10531d05cddcSAtari911        text = text ? text.trim() : link;
10541d05cddcSAtari911
10551d05cddcSAtari911        let linkHtml;
10561d05cddcSAtari911        // Handle external URLs
10571d05cddcSAtari911        if (link.match(/^https?:\/\//)) {
10581d05cddcSAtari911            linkHtml = '<a href="' + escapeHtml(link) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>';
10591d05cddcSAtari911        } else {
10601d05cddcSAtari911            // Handle internal DokuWiki links with section anchors
10611d05cddcSAtari911            const hashIndex = link.indexOf('#');
10621d05cddcSAtari911            let pagePart = link;
10631d05cddcSAtari911            let sectionPart = '';
10641d05cddcSAtari911
10651d05cddcSAtari911            if (hashIndex !== -1) {
10661d05cddcSAtari911                pagePart = link.substring(0, hashIndex);
10671d05cddcSAtari911                sectionPart = link.substring(hashIndex); // Includes the #
10681d05cddcSAtari911            }
10691d05cddcSAtari911
10701d05cddcSAtari911            const wikiUrl = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(pagePart) + sectionPart;
10711d05cddcSAtari911            linkHtml = '<a href="' + wikiUrl + '">' + escapeHtml(text) + '</a>';
10721d05cddcSAtari911        }
10731d05cddcSAtari911
10741d05cddcSAtari911        const token = '\x00TOKEN' + tokenIndex + '\x00';
10751d05cddcSAtari911        tokens[tokenIndex] = linkHtml;
10761d05cddcSAtari911        tokenIndex++;
10771d05cddcSAtari911        return token;
10781d05cddcSAtari911    });
10791d05cddcSAtari911
10801d05cddcSAtari911    // Convert markdown-style links [text](url) to tokens
10811d05cddcSAtari911    rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) {
10821d05cddcSAtari911        text = text.trim();
10831d05cddcSAtari911        url = url.trim();
10841d05cddcSAtari911
10851d05cddcSAtari911        let linkHtml;
10861d05cddcSAtari911        if (url.match(/^https?:\/\//)) {
10871d05cddcSAtari911            linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>';
10881d05cddcSAtari911        } else {
10891d05cddcSAtari911            linkHtml = '<a href="' + escapeHtml(url) + '">' + escapeHtml(text) + '</a>';
10901d05cddcSAtari911        }
10911d05cddcSAtari911
10921d05cddcSAtari911        const token = '\x00TOKEN' + tokenIndex + '\x00';
10931d05cddcSAtari911        tokens[tokenIndex] = linkHtml;
10941d05cddcSAtari911        tokenIndex++;
10951d05cddcSAtari911        return token;
10961d05cddcSAtari911    });
10971d05cddcSAtari911
10981d05cddcSAtari911    // Convert plain URLs to tokens
10991d05cddcSAtari911    rendered = rendered.replace(/(https?:\/\/[^\s<]+)/g, function(match, url) {
11001d05cddcSAtari911        const linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(url) + '</a>';
11011d05cddcSAtari911        const token = '\x00TOKEN' + tokenIndex + '\x00';
11021d05cddcSAtari911        tokens[tokenIndex] = linkHtml;
11031d05cddcSAtari911        tokenIndex++;
11041d05cddcSAtari911        return token;
11051d05cddcSAtari911    });
11061d05cddcSAtari911
11071d05cddcSAtari911    // NOW escape the remaining text (tokens are protected with null bytes)
11081d05cddcSAtari911    rendered = escapeHtml(rendered);
11091d05cddcSAtari911
11101d05cddcSAtari911    // Convert newlines to <br>
11111d05cddcSAtari911    rendered = rendered.replace(/\n/g, '<br>');
11121d05cddcSAtari911
11131d05cddcSAtari911    // DokuWiki text formatting (on escaped text)
11141d05cddcSAtari911    // Bold: **text** or __text__
11151d05cddcSAtari911    rendered = rendered.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
11161d05cddcSAtari911    rendered = rendered.replace(/__(.+?)__/g, '<strong>$1</strong>');
11171d05cddcSAtari911
11181d05cddcSAtari911    // Italic: //text//
11191d05cddcSAtari911    rendered = rendered.replace(/\/\/(.+?)\/\//g, '<em>$1</em>');
11201d05cddcSAtari911
11211d05cddcSAtari911    // Strikethrough: <del>text</del>
11221d05cddcSAtari911    rendered = rendered.replace(/&lt;del&gt;(.+?)&lt;\/del&gt;/g, '<del>$1</del>');
11231d05cddcSAtari911
11241d05cddcSAtari911    // Monospace: ''text''
11251d05cddcSAtari911    rendered = rendered.replace(/&#39;&#39;(.+?)&#39;&#39;/g, '<code>$1</code>');
11261d05cddcSAtari911
11271d05cddcSAtari911    // Subscript: <sub>text</sub>
11281d05cddcSAtari911    rendered = rendered.replace(/&lt;sub&gt;(.+?)&lt;\/sub&gt;/g, '<sub>$1</sub>');
11291d05cddcSAtari911
11301d05cddcSAtari911    // Superscript: <sup>text</sup>
11311d05cddcSAtari911    rendered = rendered.replace(/&lt;sup&gt;(.+?)&lt;\/sup&gt;/g, '<sup>$1</sup>');
11321d05cddcSAtari911
11331d05cddcSAtari911    // Restore tokens (replace with actual HTML)
11341d05cddcSAtari911    for (let i = 0; i < tokens.length; i++) {
11351d05cddcSAtari911        const tokenPattern = new RegExp('\x00TOKEN' + i + '\x00', 'g');
11361d05cddcSAtari911        rendered = rendered.replace(tokenPattern, tokens[i]);
11371d05cddcSAtari911    }
11381d05cddcSAtari911
11391d05cddcSAtari911    return rendered;
11401d05cddcSAtari911}
11411d05cddcSAtari911
11421d05cddcSAtari911// Open add event dialog
11431d05cddcSAtari911window.openAddEvent = function(calId, namespace, date) {
11441d05cddcSAtari911    const dialog = document.getElementById('dialog-' + calId);
11451d05cddcSAtari911    const form = document.getElementById('eventform-' + calId);
11461d05cddcSAtari911    const title = document.getElementById('dialog-title-' + calId);
11471d05cddcSAtari911    const dateField = document.getElementById('event-date-' + calId);
11481d05cddcSAtari911
11491d05cddcSAtari911    if (!dateField) {
11501d05cddcSAtari911        console.error('Date field not found! ID: event-date-' + calId);
11511d05cddcSAtari911        return;
11521d05cddcSAtari911    }
11531d05cddcSAtari911
1154231d0edbSAtari911    // Check if there's a filtered namespace active (only for regular calendars)
11551d05cddcSAtari911    const calendar = document.getElementById(calId);
1156231d0edbSAtari911    const filteredNamespace = calendar ? calendar.dataset.filteredNamespace : null;
11571d05cddcSAtari911
11581d05cddcSAtari911    // Use filtered namespace if available, otherwise use the passed namespace
11591d05cddcSAtari911    const effectiveNamespace = filteredNamespace || namespace;
11601d05cddcSAtari911
11611d05cddcSAtari911
11621d05cddcSAtari911    // Reset form
11631d05cddcSAtari911    form.reset();
11641d05cddcSAtari911    document.getElementById('event-id-' + calId).value = '';
11651d05cddcSAtari911
11661d05cddcSAtari911    // Store the effective namespace in a hidden field or data attribute
11671d05cddcSAtari911    form.dataset.effectiveNamespace = effectiveNamespace;
11681d05cddcSAtari911
11691d05cddcSAtari911    // Set namespace dropdown to effective namespace
11701d05cddcSAtari911    const namespaceSelect = document.getElementById('event-namespace-' + calId);
11711d05cddcSAtari911    if (namespaceSelect) {
11721d05cddcSAtari911        if (effectiveNamespace && effectiveNamespace !== '*' && effectiveNamespace.indexOf(';') === -1) {
11731d05cddcSAtari911            // Set to specific namespace if not wildcard or multi-namespace
11741d05cddcSAtari911            namespaceSelect.value = effectiveNamespace;
11751d05cddcSAtari911        } else {
11761d05cddcSAtari911            // Default to empty (default namespace) for wildcard/multi views
11771d05cddcSAtari911            namespaceSelect.value = '';
11781d05cddcSAtari911        }
11791d05cddcSAtari911    }
11801d05cddcSAtari911
11811d05cddcSAtari911    // Clear event namespace from previous edits
11821d05cddcSAtari911    delete form.dataset.eventNamespace;
11831d05cddcSAtari911
11841d05cddcSAtari911    // Set date - use local date, not UTC
11851d05cddcSAtari911    let defaultDate = date;
11861d05cddcSAtari911    if (!defaultDate) {
11871d05cddcSAtari911        // Get the currently displayed month from the calendar container
11881d05cddcSAtari911        const container = document.getElementById(calId);
11891d05cddcSAtari911        const displayedYear = parseInt(container.getAttribute('data-year'));
11901d05cddcSAtari911        const displayedMonth = parseInt(container.getAttribute('data-month'));
11911d05cddcSAtari911
11921d05cddcSAtari911
11931d05cddcSAtari911        if (displayedYear && displayedMonth) {
11941d05cddcSAtari911            // Use first day of the displayed month
11951d05cddcSAtari911            const year = displayedYear;
11961d05cddcSAtari911            const month = String(displayedMonth).padStart(2, '0');
11971d05cddcSAtari911            defaultDate = `${year}-${month}-01`;
11981d05cddcSAtari911        } else {
11991d05cddcSAtari911            // Fallback to today if attributes not found
12001d05cddcSAtari911            const today = new Date();
12011d05cddcSAtari911            const year = today.getFullYear();
12021d05cddcSAtari911            const month = String(today.getMonth() + 1).padStart(2, '0');
12031d05cddcSAtari911            const day = String(today.getDate()).padStart(2, '0');
12041d05cddcSAtari911            defaultDate = `${year}-${month}-${day}`;
12051d05cddcSAtari911        }
12061d05cddcSAtari911    }
12071d05cddcSAtari911    dateField.value = defaultDate;
12081d05cddcSAtari911    dateField.removeAttribute('data-original-date');
12091d05cddcSAtari911
12101d05cddcSAtari911    // Also set the end date field to the same default (user can change it)
12111d05cddcSAtari911    const endDateField = document.getElementById('event-end-date-' + calId);
12121d05cddcSAtari911    if (endDateField) {
12131d05cddcSAtari911        endDateField.value = ''; // Empty by default (single-day event)
12141d05cddcSAtari911        // Set min attribute to help the date picker open on the right month
12151d05cddcSAtari911        endDateField.setAttribute('min', defaultDate);
12161d05cddcSAtari911    }
12171d05cddcSAtari911
12181d05cddcSAtari911    // Set default color
12191d05cddcSAtari911    document.getElementById('event-color-' + calId).value = '#3498db';
12201d05cddcSAtari911
12211d05cddcSAtari911    // Initialize end time dropdown (disabled by default since no start time set)
12221d05cddcSAtari911    const endTimeField = document.getElementById('event-end-time-' + calId);
12231d05cddcSAtari911    if (endTimeField) {
12241d05cddcSAtari911        endTimeField.disabled = true;
12251d05cddcSAtari911        endTimeField.value = '';
12261d05cddcSAtari911    }
12271d05cddcSAtari911
12281d05cddcSAtari911    // Initialize namespace search
12291d05cddcSAtari911    initNamespaceSearch(calId);
12301d05cddcSAtari911
12311d05cddcSAtari911    // Set title
12321d05cddcSAtari911    title.textContent = 'Add Event';
12331d05cddcSAtari911
12341d05cddcSAtari911    // Show dialog
12351d05cddcSAtari911    dialog.style.display = 'flex';
12361d05cddcSAtari911
1237*9ccd446eSAtari911    // Propagate CSS vars to dialog (position:fixed can break inheritance in some templates)
1238*9ccd446eSAtari911    propagateThemeVars(calId, dialog);
1239*9ccd446eSAtari911
12401d05cddcSAtari911    // Focus title field
12411d05cddcSAtari911    setTimeout(() => {
12421d05cddcSAtari911        const titleField = document.getElementById('event-title-' + calId);
12431d05cddcSAtari911        if (titleField) titleField.focus();
12441d05cddcSAtari911    }, 100);
12451d05cddcSAtari911};
12461d05cddcSAtari911
12471d05cddcSAtari911// Edit event
12481d05cddcSAtari911window.editEvent = function(calId, eventId, date, namespace) {
12491d05cddcSAtari911    const params = new URLSearchParams({
12501d05cddcSAtari911        call: 'plugin_calendar',
12511d05cddcSAtari911        action: 'get_event',
12521d05cddcSAtari911        namespace: namespace,
12531d05cddcSAtari911        date: date,
12541d05cddcSAtari911        eventId: eventId
12551d05cddcSAtari911    });
12561d05cddcSAtari911
12571d05cddcSAtari911    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
12581d05cddcSAtari911        method: 'POST',
12591d05cddcSAtari911        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
12601d05cddcSAtari911        body: params.toString()
12611d05cddcSAtari911    })
12621d05cddcSAtari911    .then(r => r.json())
12631d05cddcSAtari911    .then(data => {
12641d05cddcSAtari911        if (data.success && data.event) {
12651d05cddcSAtari911            const event = data.event;
12661d05cddcSAtari911            const dialog = document.getElementById('dialog-' + calId);
12671d05cddcSAtari911            const title = document.getElementById('dialog-title-' + calId);
12681d05cddcSAtari911            const dateField = document.getElementById('event-date-' + calId);
12691d05cddcSAtari911            const form = document.getElementById('eventform-' + calId);
12701d05cddcSAtari911
12711d05cddcSAtari911            if (!dateField) {
12721d05cddcSAtari911                console.error('Date field not found when editing!');
12731d05cddcSAtari911                return;
12741d05cddcSAtari911            }
12751d05cddcSAtari911
12761d05cddcSAtari911            // Store the event's actual namespace for saving (important for namespace=* views)
12771d05cddcSAtari911            if (event.namespace !== undefined) {
12781d05cddcSAtari911                form.dataset.eventNamespace = event.namespace;
12791d05cddcSAtari911            }
12801d05cddcSAtari911
12811d05cddcSAtari911            // Populate form
12821d05cddcSAtari911            document.getElementById('event-id-' + calId).value = event.id;
12831d05cddcSAtari911            dateField.value = date;
12841d05cddcSAtari911            dateField.setAttribute('data-original-date', date);
12851d05cddcSAtari911
12861d05cddcSAtari911            const endDateField = document.getElementById('event-end-date-' + calId);
12871d05cddcSAtari911            endDateField.value = event.endDate || '';
12881d05cddcSAtari911            // Set min attribute to help date picker open on the start date's month
12891d05cddcSAtari911            endDateField.setAttribute('min', date);
12901d05cddcSAtari911
12911d05cddcSAtari911            document.getElementById('event-title-' + calId).value = event.title;
12921d05cddcSAtari911            document.getElementById('event-time-' + calId).value = event.time || '';
12931d05cddcSAtari911            document.getElementById('event-end-time-' + calId).value = event.endTime || '';
12941d05cddcSAtari911            document.getElementById('event-color-' + calId).value = event.color || '#3498db';
12951d05cddcSAtari911            document.getElementById('event-desc-' + calId).value = event.description || '';
12961d05cddcSAtari911            document.getElementById('event-is-task-' + calId).checked = event.isTask || false;
12971d05cddcSAtari911
12981d05cddcSAtari911            // Update end time options based on start time
12991d05cddcSAtari911            if (event.time) {
13001d05cddcSAtari911                updateEndTimeOptions(calId);
13011d05cddcSAtari911            }
13021d05cddcSAtari911
13031d05cddcSAtari911            // Initialize namespace search
13041d05cddcSAtari911            initNamespaceSearch(calId);
13051d05cddcSAtari911
13061d05cddcSAtari911            // Set namespace fields if available
13071d05cddcSAtari911            const namespaceHidden = document.getElementById('event-namespace-' + calId);
13081d05cddcSAtari911            const namespaceSearch = document.getElementById('event-namespace-search-' + calId);
13091d05cddcSAtari911            if (namespaceHidden && event.namespace !== undefined) {
1310*9ccd446eSAtari911                // Set the hidden input (this is what gets submitted)
1311*9ccd446eSAtari911                namespaceHidden.value = event.namespace || '';
1312*9ccd446eSAtari911                // Set the search input to display the namespace
13131d05cddcSAtari911                if (namespaceSearch) {
13141d05cddcSAtari911                    namespaceSearch.value = event.namespace || '(default)';
13151d05cddcSAtari911                }
1316*9ccd446eSAtari911                console.log('Set namespace for editing:', event.namespace, 'Hidden value:', namespaceHidden.value);
1317*9ccd446eSAtari911            } else {
1318*9ccd446eSAtari911                // No namespace on event, set to default
1319*9ccd446eSAtari911                if (namespaceHidden) {
1320*9ccd446eSAtari911                    namespaceHidden.value = '';
1321*9ccd446eSAtari911                }
1322*9ccd446eSAtari911                if (namespaceSearch) {
1323*9ccd446eSAtari911                    namespaceSearch.value = '(default)';
1324*9ccd446eSAtari911                }
1325*9ccd446eSAtari911                console.log('No namespace on event, using default');
13261d05cddcSAtari911            }
13271d05cddcSAtari911
13281d05cddcSAtari911            title.textContent = 'Edit Event';
13291d05cddcSAtari911            dialog.style.display = 'flex';
1330*9ccd446eSAtari911
1331*9ccd446eSAtari911            // Propagate CSS vars to dialog
1332*9ccd446eSAtari911            propagateThemeVars(calId, dialog);
13331d05cddcSAtari911        }
13341d05cddcSAtari911    })
13351d05cddcSAtari911    .catch(err => console.error('Error editing event:', err));
13361d05cddcSAtari911};
13371d05cddcSAtari911
13381d05cddcSAtari911// Delete event
13391d05cddcSAtari911window.deleteEvent = function(calId, eventId, date, namespace) {
13401d05cddcSAtari911    if (!confirm('Delete this event?')) return;
13411d05cddcSAtari911
13421d05cddcSAtari911    const params = new URLSearchParams({
13431d05cddcSAtari911        call: 'plugin_calendar',
13441d05cddcSAtari911        action: 'delete_event',
13451d05cddcSAtari911        namespace: namespace,
13461d05cddcSAtari911        date: date,
13471d05cddcSAtari911        eventId: eventId
13481d05cddcSAtari911    });
13491d05cddcSAtari911
13501d05cddcSAtari911    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
13511d05cddcSAtari911        method: 'POST',
13521d05cddcSAtari911        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
13531d05cddcSAtari911        body: params.toString()
13541d05cddcSAtari911    })
13551d05cddcSAtari911    .then(r => r.json())
13561d05cddcSAtari911    .then(data => {
13571d05cddcSAtari911        if (data.success) {
13581d05cddcSAtari911            // Extract year and month from date
13591d05cddcSAtari911            const [year, month] = date.split('-').map(Number);
13601d05cddcSAtari911
13611d05cddcSAtari911            // Reload calendar data via AJAX
13621d05cddcSAtari911            reloadCalendarData(calId, year, month, namespace);
13631d05cddcSAtari911        }
13641d05cddcSAtari911    })
13651d05cddcSAtari911    .catch(err => console.error('Error:', err));
13661d05cddcSAtari911};
13671d05cddcSAtari911
13681d05cddcSAtari911// Save event (add or edit)
13691d05cddcSAtari911window.saveEventCompact = function(calId, namespace) {
13701d05cddcSAtari911    const form = document.getElementById('eventform-' + calId);
13711d05cddcSAtari911
13721d05cddcSAtari911    // Get namespace from dropdown - this is what the user selected
13731d05cddcSAtari911    const namespaceSelect = document.getElementById('event-namespace-' + calId);
13741d05cddcSAtari911    const selectedNamespace = namespaceSelect ? namespaceSelect.value : '';
13751d05cddcSAtari911
13761d05cddcSAtari911    // ALWAYS use what the user selected in the dropdown
13771d05cddcSAtari911    // This allows changing namespace when editing
13781d05cddcSAtari911    const finalNamespace = selectedNamespace;
13791d05cddcSAtari911
13801d05cddcSAtari911    const eventId = document.getElementById('event-id-' + calId).value;
13811d05cddcSAtari911
13821d05cddcSAtari911    // eventNamespace is the ORIGINAL namespace (only used for finding/deleting old event)
13831d05cddcSAtari911    const originalNamespace = form.dataset.eventNamespace;
13841d05cddcSAtari911
13851d05cddcSAtari911
13861d05cddcSAtari911    const dateInput = document.getElementById('event-date-' + calId);
13871d05cddcSAtari911    const date = dateInput.value;
13881d05cddcSAtari911    const oldDate = dateInput.getAttribute('data-original-date') || date;
13891d05cddcSAtari911    const endDate = document.getElementById('event-end-date-' + calId).value;
13901d05cddcSAtari911    const title = document.getElementById('event-title-' + calId).value;
13911d05cddcSAtari911    const time = document.getElementById('event-time-' + calId).value;
13921d05cddcSAtari911    const endTime = document.getElementById('event-end-time-' + calId).value;
13931d05cddcSAtari911    const colorSelect = document.getElementById('event-color-' + calId);
13941d05cddcSAtari911    let color = colorSelect.value;
13951d05cddcSAtari911
13961d05cddcSAtari911    // Handle custom color
13971d05cddcSAtari911    if (color === 'custom') {
13981d05cddcSAtari911        color = colorSelect.dataset.customColor || document.getElementById('event-color-custom-' + calId).value;
13991d05cddcSAtari911    }
14001d05cddcSAtari911
14011d05cddcSAtari911    const description = document.getElementById('event-desc-' + calId).value;
14021d05cddcSAtari911    const isTask = document.getElementById('event-is-task-' + calId).checked;
14031d05cddcSAtari911    const completed = false; // New tasks are not completed
14041d05cddcSAtari911    const isRecurring = document.getElementById('event-recurring-' + calId).checked;
14051d05cddcSAtari911    const recurrenceType = document.getElementById('event-recurrence-type-' + calId).value;
14061d05cddcSAtari911    const recurrenceEnd = document.getElementById('event-recurrence-end-' + calId).value;
14071d05cddcSAtari911
14081d05cddcSAtari911    if (!title) {
14091d05cddcSAtari911        alert('Please enter a title');
14101d05cddcSAtari911        return;
14111d05cddcSAtari911    }
14121d05cddcSAtari911
14131d05cddcSAtari911    if (!date) {
14141d05cddcSAtari911        alert('Please select a date');
14151d05cddcSAtari911        return;
14161d05cddcSAtari911    }
14171d05cddcSAtari911
14181d05cddcSAtari911    const params = new URLSearchParams({
14191d05cddcSAtari911        call: 'plugin_calendar',
14201d05cddcSAtari911        action: 'save_event',
14211d05cddcSAtari911        namespace: finalNamespace,
14221d05cddcSAtari911        eventId: eventId,
14231d05cddcSAtari911        date: date,
14241d05cddcSAtari911        oldDate: oldDate,
14251d05cddcSAtari911        endDate: endDate,
14261d05cddcSAtari911        title: title,
14271d05cddcSAtari911        time: time,
14281d05cddcSAtari911        endTime: endTime,
14291d05cddcSAtari911        color: color,
14301d05cddcSAtari911        description: description,
14311d05cddcSAtari911        isTask: isTask ? '1' : '0',
14321d05cddcSAtari911        completed: completed ? '1' : '0',
14331d05cddcSAtari911        isRecurring: isRecurring ? '1' : '0',
14341d05cddcSAtari911        recurrenceType: recurrenceType,
14351d05cddcSAtari911        recurrenceEnd: recurrenceEnd
14361d05cddcSAtari911    });
14371d05cddcSAtari911
14381d05cddcSAtari911    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
14391d05cddcSAtari911        method: 'POST',
14401d05cddcSAtari911        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
14411d05cddcSAtari911        body: params.toString()
14421d05cddcSAtari911    })
14431d05cddcSAtari911    .then(r => r.json())
14441d05cddcSAtari911    .then(data => {
14451d05cddcSAtari911        if (data.success) {
14461d05cddcSAtari911            closeEventDialog(calId);
14471d05cddcSAtari911
14481d05cddcSAtari911            // For recurring events, do a full page reload to show all occurrences
14491d05cddcSAtari911            if (isRecurring) {
14501d05cddcSAtari911                location.reload();
14511d05cddcSAtari911                return;
14521d05cddcSAtari911            }
14531d05cddcSAtari911
14541d05cddcSAtari911            // Extract year and month from the NEW date (in case date was changed)
14551d05cddcSAtari911            const [year, month] = date.split('-').map(Number);
14561d05cddcSAtari911
14571d05cddcSAtari911            // Reload calendar data via AJAX to the month of the event
14581d05cddcSAtari911            reloadCalendarData(calId, year, month, namespace);
14591d05cddcSAtari911        } else {
14601d05cddcSAtari911            alert('Error: ' + (data.error || 'Unknown error'));
14611d05cddcSAtari911        }
14621d05cddcSAtari911    })
14631d05cddcSAtari911    .catch(err => {
14641d05cddcSAtari911        console.error('Error:', err);
14651d05cddcSAtari911        alert('Error saving event');
14661d05cddcSAtari911    });
14671d05cddcSAtari911};
14681d05cddcSAtari911
14691d05cddcSAtari911// Reload calendar data without page refresh
14701d05cddcSAtari911window.reloadCalendarData = function(calId, year, month, namespace) {
14711d05cddcSAtari911    const params = new URLSearchParams({
14721d05cddcSAtari911        call: 'plugin_calendar',
14731d05cddcSAtari911        action: 'load_month',
14741d05cddcSAtari911        year: year,
14751d05cddcSAtari911        month: month,
14761d05cddcSAtari911        namespace: namespace,
14771d05cddcSAtari911        _: new Date().getTime() // Cache buster
14781d05cddcSAtari911    });
14791d05cddcSAtari911
14801d05cddcSAtari911    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
14811d05cddcSAtari911        method: 'POST',
14821d05cddcSAtari911        headers: {
14831d05cddcSAtari911            'Content-Type': 'application/x-www-form-urlencoded',
14841d05cddcSAtari911            'Cache-Control': 'no-cache, no-store, must-revalidate',
14851d05cddcSAtari911            'Pragma': 'no-cache'
14861d05cddcSAtari911        },
14871d05cddcSAtari911        body: params.toString()
14881d05cddcSAtari911    })
14891d05cddcSAtari911    .then(r => r.json())
14901d05cddcSAtari911    .then(data => {
14911d05cddcSAtari911        if (data.success) {
14921d05cddcSAtari911            const container = document.getElementById(calId);
14931d05cddcSAtari911
14941d05cddcSAtari911            // Check if this is a full calendar or just event panel
14951d05cddcSAtari911            if (container.classList.contains('calendar-compact-container')) {
14961d05cddcSAtari911                rebuildCalendar(calId, data.year, data.month, data.events, namespace);
14971d05cddcSAtari911            } else if (container.classList.contains('event-panel-standalone')) {
14981d05cddcSAtari911                rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
14991d05cddcSAtari911            }
15001d05cddcSAtari911        }
15011d05cddcSAtari911    })
15021d05cddcSAtari911    .catch(err => console.error('Error:', err));
15031d05cddcSAtari911};
15041d05cddcSAtari911
15051d05cddcSAtari911// Close event dialog
15061d05cddcSAtari911window.closeEventDialog = function(calId) {
15071d05cddcSAtari911    const dialog = document.getElementById('dialog-' + calId);
15081d05cddcSAtari911    dialog.style.display = 'none';
15091d05cddcSAtari911};
15101d05cddcSAtari911
15111d05cddcSAtari911// Escape HTML
15121d05cddcSAtari911window.escapeHtml = function(text) {
15131d05cddcSAtari911    const div = document.createElement('div');
15141d05cddcSAtari911    div.textContent = text;
15151d05cddcSAtari911    return div.innerHTML;
15161d05cddcSAtari911};
15171d05cddcSAtari911
15181d05cddcSAtari911// Highlight event when clicking on bar in calendar
15191d05cddcSAtari911window.highlightEvent = function(calId, eventId, date) {
1520*9ccd446eSAtari911    console.log('Highlighting event:', calId, eventId, date);
1521*9ccd446eSAtari911
15221d05cddcSAtari911    // Find the event item in the event list
15231d05cddcSAtari911    const eventList = document.querySelector('#' + calId + ' .event-list-compact');
1524*9ccd446eSAtari911    if (!eventList) {
1525*9ccd446eSAtari911        console.log('Event list not found');
1526*9ccd446eSAtari911        return;
1527*9ccd446eSAtari911    }
15281d05cddcSAtari911
15291d05cddcSAtari911    const eventItem = eventList.querySelector('[data-event-id="' + eventId + '"][data-date="' + date + '"]');
1530*9ccd446eSAtari911    if (!eventItem) {
1531*9ccd446eSAtari911        console.log('Event item not found');
1532*9ccd446eSAtari911        return;
1533*9ccd446eSAtari911    }
15341d05cddcSAtari911
1535*9ccd446eSAtari911    console.log('Found event item:', eventItem);
1536*9ccd446eSAtari911
1537*9ccd446eSAtari911    // Get theme
1538*9ccd446eSAtari911    const container = document.getElementById(calId);
1539*9ccd446eSAtari911    const theme = container ? container.dataset.theme : 'matrix';
1540*9ccd446eSAtari911    const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {};
1541*9ccd446eSAtari911
1542*9ccd446eSAtari911    console.log('Theme:', theme);
1543*9ccd446eSAtari911
1544*9ccd446eSAtari911    // Theme-specific highlight colors
1545*9ccd446eSAtari911    let highlightBg, highlightShadow;
1546*9ccd446eSAtari911    if (theme === 'matrix') {
1547*9ccd446eSAtari911        highlightBg = '#1a3d1a';  // Darker green
1548*9ccd446eSAtari911        highlightShadow = '0 0 20px rgba(0, 204, 7, 0.8), 0 0 40px rgba(0, 204, 7, 0.4)';
1549*9ccd446eSAtari911    } else if (theme === 'purple') {
1550*9ccd446eSAtari911        highlightBg = '#3d2b4d';  // Darker purple
1551*9ccd446eSAtari911        highlightShadow = '0 0 20px rgba(155, 89, 182, 0.8), 0 0 40px rgba(155, 89, 182, 0.4)';
1552*9ccd446eSAtari911    } else if (theme === 'professional') {
1553*9ccd446eSAtari911        highlightBg = '#e3f2fd';  // Light blue
1554*9ccd446eSAtari911        highlightShadow = '0 0 20px rgba(74, 144, 226, 0.4)';
1555*9ccd446eSAtari911    } else if (theme === 'pink') {
1556*9ccd446eSAtari911        highlightBg = '#3d2030';  // Darker pink
1557*9ccd446eSAtari911        highlightShadow = '0 0 20px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4)';
1558*9ccd446eSAtari911    } else if (theme === 'wiki') {
1559*9ccd446eSAtari911        highlightBg = '#dce9f5';  // Light blue highlight
1560*9ccd446eSAtari911        highlightShadow = '0 0 20px rgba(43, 115, 183, 0.4)';
1561*9ccd446eSAtari911    }
1562*9ccd446eSAtari911
1563*9ccd446eSAtari911    console.log('Highlight colors:', highlightBg, highlightShadow);
1564*9ccd446eSAtari911
1565*9ccd446eSAtari911    // Store original styles
1566*9ccd446eSAtari911    const originalBg = eventItem.style.background;
1567*9ccd446eSAtari911    const originalShadow = eventItem.style.boxShadow;
1568*9ccd446eSAtari911
1569*9ccd446eSAtari911    // Remove previous highlights (restore their original styles)
15701d05cddcSAtari911    const previousHighlights = eventList.querySelectorAll('.event-highlighted');
1571*9ccd446eSAtari911    previousHighlights.forEach(el => {
1572*9ccd446eSAtari911        el.classList.remove('event-highlighted');
1573*9ccd446eSAtari911    });
15741d05cddcSAtari911
1575*9ccd446eSAtari911    // Add highlight class and apply theme-aware glow
15761d05cddcSAtari911    eventItem.classList.add('event-highlighted');
15771d05cddcSAtari911
1578*9ccd446eSAtari911    // Set CSS properties directly
1579*9ccd446eSAtari911    eventItem.style.setProperty('background', highlightBg, 'important');
1580*9ccd446eSAtari911    eventItem.style.setProperty('box-shadow', highlightShadow, 'important');
1581*9ccd446eSAtari911    eventItem.style.setProperty('transition', 'all 0.3s ease-in-out', 'important');
1582*9ccd446eSAtari911
1583*9ccd446eSAtari911    console.log('Applied highlight styles');
1584*9ccd446eSAtari911
15851d05cddcSAtari911    // Scroll to event
15861d05cddcSAtari911    eventItem.scrollIntoView({
15871d05cddcSAtari911        behavior: 'smooth',
15881d05cddcSAtari911        block: 'nearest',
15891d05cddcSAtari911        inline: 'nearest'
15901d05cddcSAtari911    });
15911d05cddcSAtari911
1592*9ccd446eSAtari911    // Remove highlight after 3 seconds and restore original styles
15931d05cddcSAtari911    setTimeout(() => {
1594*9ccd446eSAtari911        console.log('Removing highlight');
15951d05cddcSAtari911        eventItem.classList.remove('event-highlighted');
1596*9ccd446eSAtari911        eventItem.style.setProperty('background', originalBg);
1597*9ccd446eSAtari911        eventItem.style.setProperty('box-shadow', originalShadow);
1598*9ccd446eSAtari911        eventItem.style.setProperty('transition', '');
15991d05cddcSAtari911    }, 3000);
16001d05cddcSAtari911};
16011d05cddcSAtari911
16021d05cddcSAtari911// Toggle recurring event options
16031d05cddcSAtari911window.toggleRecurringOptions = function(calId) {
16041d05cddcSAtari911    const checkbox = document.getElementById('event-recurring-' + calId);
16051d05cddcSAtari911    const options = document.getElementById('recurring-options-' + calId);
16061d05cddcSAtari911
16071d05cddcSAtari911    if (checkbox && options) {
16081d05cddcSAtari911        options.style.display = checkbox.checked ? 'block' : 'none';
16091d05cddcSAtari911    }
16101d05cddcSAtari911};
16111d05cddcSAtari911
1612*9ccd446eSAtari911// ============================================================
1613*9ccd446eSAtari911// Document-level event delegation (guarded - only attach once)
1614*9ccd446eSAtari911// These use event delegation so they work for AJAX-rebuilt content.
1615*9ccd446eSAtari911// ============================================================
1616*9ccd446eSAtari911if (!window._calendarDelegationInit) {
1617*9ccd446eSAtari911    window._calendarDelegationInit = true;
1618*9ccd446eSAtari911
1619*9ccd446eSAtari911    // ESC closes dialogs, popups, tooltips
16201d05cddcSAtari911    document.addEventListener('keydown', function(e) {
16211d05cddcSAtari911        if (e.key === 'Escape') {
1622*9ccd446eSAtari911            document.querySelectorAll('.event-dialog-compact').forEach(function(d) {
1623*9ccd446eSAtari911                if (d.style.display === 'flex') d.style.display = 'none';
1624*9ccd446eSAtari911            });
1625*9ccd446eSAtari911            document.querySelectorAll('.day-popup').forEach(function(p) {
1626*9ccd446eSAtari911                p.style.display = 'none';
1627*9ccd446eSAtari911            });
1628*9ccd446eSAtari911            hideConflictTooltip();
16291d05cddcSAtari911        }
16301d05cddcSAtari911    });
1631*9ccd446eSAtari911
1632*9ccd446eSAtari911    // Conflict tooltip delegation (capture phase for mouseenter/leave)
1633*9ccd446eSAtari911    document.addEventListener('mouseenter', function(e) {
1634*9ccd446eSAtari911        if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) {
1635*9ccd446eSAtari911            showConflictTooltip(e.target);
16361d05cddcSAtari911        }
1637*9ccd446eSAtari911    }, true);
1638*9ccd446eSAtari911
1639*9ccd446eSAtari911    document.addEventListener('mouseleave', function(e) {
1640*9ccd446eSAtari911        if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) {
1641*9ccd446eSAtari911            hideConflictTooltip();
1642*9ccd446eSAtari911        }
1643*9ccd446eSAtari911    }, true);
1644*9ccd446eSAtari911} // end delegation guard
16451d05cddcSAtari911
16461d05cddcSAtari911// Event panel navigation
16471d05cddcSAtari911window.navEventPanel = function(calId, year, month, namespace) {
16481d05cddcSAtari911    const params = new URLSearchParams({
16491d05cddcSAtari911        call: 'plugin_calendar',
16501d05cddcSAtari911        action: 'load_month',
16511d05cddcSAtari911        year: year,
16521d05cddcSAtari911        month: month,
16531d05cddcSAtari911        namespace: namespace,
16541d05cddcSAtari911        _: new Date().getTime() // Cache buster
16551d05cddcSAtari911    });
16561d05cddcSAtari911
16571d05cddcSAtari911    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
16581d05cddcSAtari911        method: 'POST',
16591d05cddcSAtari911        headers: {
16601d05cddcSAtari911            'Content-Type': 'application/x-www-form-urlencoded',
16611d05cddcSAtari911            'Cache-Control': 'no-cache, no-store, must-revalidate',
16621d05cddcSAtari911            'Pragma': 'no-cache'
16631d05cddcSAtari911        },
16641d05cddcSAtari911        body: params.toString()
16651d05cddcSAtari911    })
16661d05cddcSAtari911    .then(r => r.json())
16671d05cddcSAtari911    .then(data => {
16681d05cddcSAtari911        if (data.success) {
16691d05cddcSAtari911            rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
16701d05cddcSAtari911        }
16711d05cddcSAtari911    })
16721d05cddcSAtari911    .catch(err => console.error('Error:', err));
16731d05cddcSAtari911};
16741d05cddcSAtari911
16751d05cddcSAtari911// Rebuild event panel only
16761d05cddcSAtari911window.rebuildEventPanel = function(calId, year, month, events, namespace) {
16771d05cddcSAtari911    const container = document.getElementById(calId);
16781d05cddcSAtari911    const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
16791d05cddcSAtari911                       'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
16801d05cddcSAtari911
16811d05cddcSAtari911    // Update month title in new compact header
16821d05cddcSAtari911    const monthTitle = container.querySelector('.panel-month-title');
16831d05cddcSAtari911    if (monthTitle) {
16841d05cddcSAtari911        monthTitle.textContent = monthNames[month - 1] + ' ' + year;
16851d05cddcSAtari911        monthTitle.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`);
16861d05cddcSAtari911        monthTitle.setAttribute('title', 'Click to jump to month');
16871d05cddcSAtari911    }
16881d05cddcSAtari911
16891d05cddcSAtari911    // Fallback: Update old header format if exists
16901d05cddcSAtari911    const oldHeader = container.querySelector('.panel-standalone-header h3, .calendar-month-picker');
16911d05cddcSAtari911    if (oldHeader && !monthTitle) {
16921d05cddcSAtari911        oldHeader.textContent = monthNames[month - 1] + ' ' + year + ' Events';
16931d05cddcSAtari911        oldHeader.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`);
16941d05cddcSAtari911    }
16951d05cddcSAtari911
16961d05cddcSAtari911    // Update nav buttons
16971d05cddcSAtari911    let prevMonth = month - 1;
16981d05cddcSAtari911    let prevYear = year;
16991d05cddcSAtari911    if (prevMonth < 1) {
17001d05cddcSAtari911        prevMonth = 12;
17011d05cddcSAtari911        prevYear--;
17021d05cddcSAtari911    }
17031d05cddcSAtari911
17041d05cddcSAtari911    let nextMonth = month + 1;
17051d05cddcSAtari911    let nextYear = year;
17061d05cddcSAtari911    if (nextMonth > 12) {
17071d05cddcSAtari911        nextMonth = 1;
17081d05cddcSAtari911        nextYear++;
17091d05cddcSAtari911    }
17101d05cddcSAtari911
17111d05cddcSAtari911    // Update new compact nav buttons
17121d05cddcSAtari911    const navBtns = container.querySelectorAll('.panel-nav-btn');
17131d05cddcSAtari911    if (navBtns[0]) navBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
17141d05cddcSAtari911    if (navBtns[1]) navBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
17151d05cddcSAtari911
17161d05cddcSAtari911    // Fallback for old nav buttons
17171d05cddcSAtari911    const oldNavBtns = container.querySelectorAll('.cal-nav-btn');
17181d05cddcSAtari911    if (oldNavBtns.length > 0 && navBtns.length === 0) {
17191d05cddcSAtari911        if (oldNavBtns[0]) oldNavBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
17201d05cddcSAtari911        if (oldNavBtns[1]) oldNavBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
17211d05cddcSAtari911    }
17221d05cddcSAtari911
17231d05cddcSAtari911    // Update Today button (works for both old and new)
17241d05cddcSAtari911    const todayBtn = container.querySelector('.panel-today-btn, .cal-today-btn, .cal-today-btn-compact');
17251d05cddcSAtari911    if (todayBtn) {
17261d05cddcSAtari911        todayBtn.setAttribute('onclick', `jumpTodayPanel('${calId}', '${namespace}')`);
17271d05cddcSAtari911    }
17281d05cddcSAtari911
17291d05cddcSAtari911    // Rebuild event list
17301d05cddcSAtari911    const eventList = container.querySelector('.event-list-compact');
17311d05cddcSAtari911    if (eventList) {
17321d05cddcSAtari911        eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month);
17331d05cddcSAtari911    }
17341d05cddcSAtari911};
17351d05cddcSAtari911
17361d05cddcSAtari911// Open add event for panel
17371d05cddcSAtari911window.openAddEventPanel = function(calId, namespace) {
17381d05cddcSAtari911    const today = new Date();
17391d05cddcSAtari911    const year = today.getFullYear();
17401d05cddcSAtari911    const month = String(today.getMonth() + 1).padStart(2, '0');
17411d05cddcSAtari911    const day = String(today.getDate()).padStart(2, '0');
17421d05cddcSAtari911    const localDate = `${year}-${month}-${day}`;
17431d05cddcSAtari911    openAddEvent(calId, namespace, localDate);
17441d05cddcSAtari911};
17451d05cddcSAtari911
17461d05cddcSAtari911// Toggle task completion
17471d05cddcSAtari911window.toggleTaskComplete = function(calId, eventId, date, namespace, completed) {
17481d05cddcSAtari911    const params = new URLSearchParams({
17491d05cddcSAtari911        call: 'plugin_calendar',
17501d05cddcSAtari911        action: 'toggle_task',
17511d05cddcSAtari911        namespace: namespace,
17521d05cddcSAtari911        date: date,
17531d05cddcSAtari911        eventId: eventId,
17541d05cddcSAtari911        completed: completed ? '1' : '0'
17551d05cddcSAtari911    });
17561d05cddcSAtari911
17571d05cddcSAtari911    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
17581d05cddcSAtari911        method: 'POST',
17591d05cddcSAtari911        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
17601d05cddcSAtari911        body: params.toString()
17611d05cddcSAtari911    })
17621d05cddcSAtari911    .then(r => r.json())
17631d05cddcSAtari911    .then(data => {
17641d05cddcSAtari911        if (data.success) {
17651d05cddcSAtari911            const [year, month] = date.split('-').map(Number);
17661d05cddcSAtari911            reloadCalendarData(calId, year, month, namespace);
17671d05cddcSAtari911        }
17681d05cddcSAtari911    })
17691d05cddcSAtari911    .catch(err => console.error('Error toggling task:', err));
17701d05cddcSAtari911};
17711d05cddcSAtari911
17721d05cddcSAtari911// Make dialog draggable
17731d05cddcSAtari911window.makeDialogDraggable = function(calId) {
17741d05cddcSAtari911    const dialog = document.getElementById('dialog-content-' + calId);
17751d05cddcSAtari911    const handle = document.getElementById('drag-handle-' + calId);
17761d05cddcSAtari911
17771d05cddcSAtari911    if (!dialog || !handle) return;
17781d05cddcSAtari911
17791d05cddcSAtari911    let isDragging = false;
17801d05cddcSAtari911    let currentX;
17811d05cddcSAtari911    let currentY;
17821d05cddcSAtari911    let initialX;
17831d05cddcSAtari911    let initialY;
17841d05cddcSAtari911    let xOffset = 0;
17851d05cddcSAtari911    let yOffset = 0;
17861d05cddcSAtari911
17871d05cddcSAtari911    handle.addEventListener('mousedown', dragStart);
17881d05cddcSAtari911    document.addEventListener('mousemove', drag);
17891d05cddcSAtari911    document.addEventListener('mouseup', dragEnd);
17901d05cddcSAtari911
17911d05cddcSAtari911    function dragStart(e) {
17921d05cddcSAtari911        initialX = e.clientX - xOffset;
17931d05cddcSAtari911        initialY = e.clientY - yOffset;
17941d05cddcSAtari911        isDragging = true;
17951d05cddcSAtari911    }
17961d05cddcSAtari911
17971d05cddcSAtari911    function drag(e) {
17981d05cddcSAtari911        if (isDragging) {
17991d05cddcSAtari911            e.preventDefault();
18001d05cddcSAtari911            currentX = e.clientX - initialX;
18011d05cddcSAtari911            currentY = e.clientY - initialY;
18021d05cddcSAtari911            xOffset = currentX;
18031d05cddcSAtari911            yOffset = currentY;
18041d05cddcSAtari911            setTranslate(currentX, currentY, dialog);
18051d05cddcSAtari911        }
18061d05cddcSAtari911    }
18071d05cddcSAtari911
18081d05cddcSAtari911    function dragEnd(e) {
18091d05cddcSAtari911        initialX = currentX;
18101d05cddcSAtari911        initialY = currentY;
18111d05cddcSAtari911        isDragging = false;
18121d05cddcSAtari911    }
18131d05cddcSAtari911
18141d05cddcSAtari911    function setTranslate(xPos, yPos, el) {
18151d05cddcSAtari911        el.style.transform = `translate(${xPos}px, ${yPos}px)`;
18161d05cddcSAtari911    }
18171d05cddcSAtari911};
18181d05cddcSAtari911
18191d05cddcSAtari911// Initialize dialog draggability when opened (avoid duplicate declaration)
18201d05cddcSAtari911if (!window.calendarDraggabilityPatched) {
18211d05cddcSAtari911    window.calendarDraggabilityPatched = true;
18221d05cddcSAtari911
18231d05cddcSAtari911    const originalOpenAddEvent = openAddEvent;
18241d05cddcSAtari911    openAddEvent = function(calId, namespace, date) {
18251d05cddcSAtari911        originalOpenAddEvent(calId, namespace, date);
18261d05cddcSAtari911        setTimeout(() => makeDialogDraggable(calId), 100);
18271d05cddcSAtari911    };
18281d05cddcSAtari911
18291d05cddcSAtari911    const originalEditEvent = editEvent;
18301d05cddcSAtari911    editEvent = function(calId, eventId, date, namespace) {
18311d05cddcSAtari911        originalEditEvent(calId, eventId, date, namespace);
18321d05cddcSAtari911        setTimeout(() => makeDialogDraggable(calId), 100);
18331d05cddcSAtari911    };
18341d05cddcSAtari911}
18351d05cddcSAtari911
18361d05cddcSAtari911// Toggle expand/collapse for past events
18371d05cddcSAtari911window.togglePastEventExpand = function(element) {
18381d05cddcSAtari911    // Stop propagation to prevent any parent click handlers
18391d05cddcSAtari911    event.stopPropagation();
18401d05cddcSAtari911
18411d05cddcSAtari911    const meta = element.querySelector(".event-meta-compact");
18421d05cddcSAtari911    const desc = element.querySelector(".event-desc-compact");
18431d05cddcSAtari911
18441d05cddcSAtari911    // Toggle visibility
18451d05cddcSAtari911    if (meta.style.display === "none") {
18461d05cddcSAtari911        // Expand
18471d05cddcSAtari911        meta.style.display = "block";
18481d05cddcSAtari911        if (desc) desc.style.display = "block";
18491d05cddcSAtari911        element.classList.add("event-past-expanded");
18501d05cddcSAtari911    } else {
18511d05cddcSAtari911        // Collapse
18521d05cddcSAtari911        meta.style.display = "none";
18531d05cddcSAtari911        if (desc) desc.style.display = "none";
18541d05cddcSAtari911        element.classList.remove("event-past-expanded");
18551d05cddcSAtari911    }
18561d05cddcSAtari911};
18571d05cddcSAtari911
1858*9ccd446eSAtari911// Filter calendar by namespace when clicking namespace badge (guarded)
1859*9ccd446eSAtari911if (!window._calendarClickDelegationInit) {
1860*9ccd446eSAtari911    window._calendarClickDelegationInit = true;
18611d05cddcSAtari911    document.addEventListener('click', function(e) {
18621d05cddcSAtari911    if (e.target.classList.contains('event-namespace-badge')) {
18631d05cddcSAtari911        const namespace = e.target.textContent;
18641d05cddcSAtari911        const eventItem = e.target.closest('.event-compact-item');
18651d05cddcSAtari911        const eventList = e.target.closest('.event-list-compact');
18661d05cddcSAtari911        const calendar = e.target.closest('.calendar-compact-container');
18671d05cddcSAtari911
18681d05cddcSAtari911        if (!eventList || !calendar) return;
18691d05cddcSAtari911
18701d05cddcSAtari911        const calId = calendar.id;
18711d05cddcSAtari911
18721d05cddcSAtari911        // Check if already filtered
18731d05cddcSAtari911        const isFiltered = eventList.classList.contains('namespace-filtered');
18741d05cddcSAtari911
18751d05cddcSAtari911        if (isFiltered && eventList.dataset.filterNamespace === namespace) {
18761d05cddcSAtari911            // Unfilter - show all
18771d05cddcSAtari911            eventList.classList.remove('namespace-filtered');
18781d05cddcSAtari911            delete eventList.dataset.filterNamespace;
18791d05cddcSAtari911            delete calendar.dataset.filteredNamespace;
18801d05cddcSAtari911            eventList.querySelectorAll('.event-compact-item').forEach(item => {
18811d05cddcSAtari911                item.style.display = '';
18821d05cddcSAtari911            });
18831d05cddcSAtari911
18841d05cddcSAtari911            // Update header to show "all namespaces"
18851d05cddcSAtari911            updateFilteredNamespaceDisplay(calId, null);
18861d05cddcSAtari911        } else {
18871d05cddcSAtari911            // Filter by this namespace
18881d05cddcSAtari911            eventList.classList.add('namespace-filtered');
18891d05cddcSAtari911            eventList.dataset.filterNamespace = namespace;
18901d05cddcSAtari911            calendar.dataset.filteredNamespace = namespace;
18911d05cddcSAtari911            eventList.querySelectorAll('.event-compact-item').forEach(item => {
18921d05cddcSAtari911                const itemBadge = item.querySelector('.event-namespace-badge');
18931d05cddcSAtari911                if (itemBadge && itemBadge.textContent === namespace) {
18941d05cddcSAtari911                    item.style.display = '';
18951d05cddcSAtari911                } else {
18961d05cddcSAtari911                    item.style.display = 'none';
18971d05cddcSAtari911                }
18981d05cddcSAtari911            });
18991d05cddcSAtari911
19001d05cddcSAtari911            // Update header to show filtered namespace
19011d05cddcSAtari911            updateFilteredNamespaceDisplay(calId, namespace);
19021d05cddcSAtari911        }
19031d05cddcSAtari911    }
19041d05cddcSAtari911    });
1905*9ccd446eSAtari911} // end click delegation guard
19061d05cddcSAtari911
19071d05cddcSAtari911// Update the displayed filtered namespace in event list header
19081d05cddcSAtari911window.updateFilteredNamespaceDisplay = function(calId, namespace) {
19091d05cddcSAtari911    const calendar = document.getElementById(calId);
19101d05cddcSAtari911    if (!calendar) return;
19111d05cddcSAtari911
19121d05cddcSAtari911    const headerContent = calendar.querySelector('.event-list-header-content');
19131d05cddcSAtari911    if (!headerContent) return;
19141d05cddcSAtari911
19151d05cddcSAtari911    // Remove existing filter badge
19161d05cddcSAtari911    let filterBadge = headerContent.querySelector('.namespace-filter-badge');
19171d05cddcSAtari911    if (filterBadge) {
19181d05cddcSAtari911        filterBadge.remove();
19191d05cddcSAtari911    }
19201d05cddcSAtari911
19211d05cddcSAtari911    // Add new filter badge if filtering
19221d05cddcSAtari911    if (namespace) {
19231d05cddcSAtari911        filterBadge = document.createElement('span');
19241d05cddcSAtari911        filterBadge.className = 'namespace-badge namespace-filter-badge';
19251d05cddcSAtari911        filterBadge.innerHTML = escapeHtml(namespace) + ' <button class="filter-clear-inline" onclick="clearNamespaceFilter(\'' + calId + '\'); event.stopPropagation();">✕</button>';
19261d05cddcSAtari911        headerContent.appendChild(filterBadge);
19271d05cddcSAtari911    }
19281d05cddcSAtari911};
19291d05cddcSAtari911
19301d05cddcSAtari911// Clear namespace filter
19311d05cddcSAtari911window.clearNamespaceFilter = function(calId) {
19321d05cddcSAtari911
19331d05cddcSAtari911    const container = document.getElementById(calId);
19341d05cddcSAtari911    if (!container) {
19351d05cddcSAtari911        console.error('Calendar container not found:', calId);
19361d05cddcSAtari911        return;
19371d05cddcSAtari911    }
19381d05cddcSAtari911
1939*9ccd446eSAtari911    // Immediately hide/remove the filter badge
1940*9ccd446eSAtari911    const filterBadge = container.querySelector('.calendar-namespace-filter');
1941*9ccd446eSAtari911    if (filterBadge) {
1942*9ccd446eSAtari911        filterBadge.style.display = 'none';
1943*9ccd446eSAtari911        filterBadge.remove();
1944*9ccd446eSAtari911    }
1945*9ccd446eSAtari911
19461d05cddcSAtari911    // Get current year and month
19471d05cddcSAtari911    const year = parseInt(container.dataset.year) || new Date().getFullYear();
19481d05cddcSAtari911    const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1);
19491d05cddcSAtari911
19501d05cddcSAtari911    // Get original namespace (what the calendar was initialized with)
19511d05cddcSAtari911    const originalNamespace = container.dataset.originalNamespace || '';
19521d05cddcSAtari911
1953*9ccd446eSAtari911    // Also check for sidebar widget
1954*9ccd446eSAtari911    const sidebarContainer = document.getElementById('sidebar-widget-' + calId);
1955*9ccd446eSAtari911    if (sidebarContainer) {
1956*9ccd446eSAtari911        // For sidebar widget, just reload the page without namespace filter
1957*9ccd446eSAtari911        // Remove the namespace from the URL and reload
1958*9ccd446eSAtari911        const url = new URL(window.location.href);
1959*9ccd446eSAtari911        url.searchParams.delete('namespace');
1960*9ccd446eSAtari911        window.location.href = url.toString();
1961*9ccd446eSAtari911        return;
1962*9ccd446eSAtari911    }
19631d05cddcSAtari911
1964*9ccd446eSAtari911    // For regular calendar, reload calendar with original namespace
19651d05cddcSAtari911    navCalendar(calId, year, month, originalNamespace);
19661d05cddcSAtari911};
19671d05cddcSAtari911
19681d05cddcSAtari911window.clearNamespaceFilterPanel = function(calId) {
19691d05cddcSAtari911
19701d05cddcSAtari911    const container = document.getElementById(calId);
19711d05cddcSAtari911    if (!container) {
19721d05cddcSAtari911        console.error('Event panel container not found:', calId);
19731d05cddcSAtari911        return;
19741d05cddcSAtari911    }
19751d05cddcSAtari911
19761d05cddcSAtari911    // Get current year and month from URL params or container
19771d05cddcSAtari911    const year = parseInt(container.dataset.year) || new Date().getFullYear();
19781d05cddcSAtari911    const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1);
19791d05cddcSAtari911
19801d05cddcSAtari911    // Get original namespace (what the panel was initialized with)
19811d05cddcSAtari911    const originalNamespace = container.dataset.originalNamespace || '';
19821d05cddcSAtari911
19831d05cddcSAtari911
19841d05cddcSAtari911    // Reload event panel with original namespace
19851d05cddcSAtari911    navEventPanel(calId, year, month, originalNamespace);
19861d05cddcSAtari911};
19871d05cddcSAtari911
19881d05cddcSAtari911// Color picker functions
19891d05cddcSAtari911window.updateCustomColorPicker = function(calId) {
19901d05cddcSAtari911    const select = document.getElementById('event-color-' + calId);
19911d05cddcSAtari911    const picker = document.getElementById('event-color-custom-' + calId);
19921d05cddcSAtari911
19931d05cddcSAtari911    if (select.value === 'custom') {
19941d05cddcSAtari911        // Show color picker
19951d05cddcSAtari911        picker.style.display = 'inline-block';
19961d05cddcSAtari911        picker.click(); // Open color picker
19971d05cddcSAtari911    } else {
19981d05cddcSAtari911        // Hide color picker and sync value
19991d05cddcSAtari911        picker.style.display = 'none';
20001d05cddcSAtari911        picker.value = select.value;
20011d05cddcSAtari911    }
20021d05cddcSAtari911};
20031d05cddcSAtari911
20041d05cddcSAtari911function updateColorFromPicker(calId) {
20051d05cddcSAtari911    const select = document.getElementById('event-color-' + calId);
20061d05cddcSAtari911    const picker = document.getElementById('event-color-custom-' + calId);
20071d05cddcSAtari911
20081d05cddcSAtari911    // Set select to custom and update its underlying value
20091d05cddcSAtari911    select.value = 'custom';
20101d05cddcSAtari911    // Store the actual color value in a data attribute
20111d05cddcSAtari911    select.dataset.customColor = picker.value;
20121d05cddcSAtari911}
20131d05cddcSAtari911
20141d05cddcSAtari911// Toggle past events visibility
20151d05cddcSAtari911window.togglePastEvents = function(calId) {
20161d05cddcSAtari911    const content = document.getElementById('past-events-' + calId);
20171d05cddcSAtari911    const arrow = document.getElementById('past-arrow-' + calId);
20181d05cddcSAtari911
20191d05cddcSAtari911    if (!content || !arrow) {
20201d05cddcSAtari911        console.error('Past events elements not found for:', calId);
20211d05cddcSAtari911        return;
20221d05cddcSAtari911    }
20231d05cddcSAtari911
20241d05cddcSAtari911    // Check computed style instead of inline style
20251d05cddcSAtari911    const isHidden = window.getComputedStyle(content).display === 'none';
20261d05cddcSAtari911
20271d05cddcSAtari911    if (isHidden) {
20281d05cddcSAtari911        content.style.display = 'block';
20291d05cddcSAtari911        arrow.textContent = '▼';
20301d05cddcSAtari911    } else {
20311d05cddcSAtari911        content.style.display = 'none';
20321d05cddcSAtari911        arrow.textContent = '▶';
20331d05cddcSAtari911    }
20341d05cddcSAtari911};
20351d05cddcSAtari911
20361d05cddcSAtari911// Fuzzy match scoring function
20371d05cddcSAtari911window.fuzzyMatch = function(pattern, str) {
20381d05cddcSAtari911    pattern = pattern.toLowerCase();
20391d05cddcSAtari911    str = str.toLowerCase();
20401d05cddcSAtari911
20411d05cddcSAtari911    let patternIdx = 0;
20421d05cddcSAtari911    let score = 0;
20431d05cddcSAtari911    let consecutiveMatches = 0;
20441d05cddcSAtari911
20451d05cddcSAtari911    for (let i = 0; i < str.length; i++) {
20461d05cddcSAtari911        if (patternIdx < pattern.length && str[i] === pattern[patternIdx]) {
20471d05cddcSAtari911            score += 1 + consecutiveMatches;
20481d05cddcSAtari911            consecutiveMatches++;
20491d05cddcSAtari911            patternIdx++;
20501d05cddcSAtari911        } else {
20511d05cddcSAtari911            consecutiveMatches = 0;
20521d05cddcSAtari911        }
20531d05cddcSAtari911    }
20541d05cddcSAtari911
20551d05cddcSAtari911    // Return null if not all characters matched
20561d05cddcSAtari911    if (patternIdx !== pattern.length) {
20571d05cddcSAtari911        return null;
20581d05cddcSAtari911    }
20591d05cddcSAtari911
20601d05cddcSAtari911    // Bonus for exact match
20611d05cddcSAtari911    if (str === pattern) {
20621d05cddcSAtari911        score += 100;
20631d05cddcSAtari911    }
20641d05cddcSAtari911
20651d05cddcSAtari911    // Bonus for starts with
20661d05cddcSAtari911    if (str.startsWith(pattern)) {
20671d05cddcSAtari911        score += 50;
20681d05cddcSAtari911    }
20691d05cddcSAtari911
20701d05cddcSAtari911    return score;
20711d05cddcSAtari911};
20721d05cddcSAtari911
20731d05cddcSAtari911// Initialize namespace search for a calendar
20741d05cddcSAtari911window.initNamespaceSearch = function(calId) {
20751d05cddcSAtari911    const searchInput = document.getElementById('event-namespace-search-' + calId);
20761d05cddcSAtari911    const hiddenInput = document.getElementById('event-namespace-' + calId);
20771d05cddcSAtari911    const dropdown = document.getElementById('event-namespace-dropdown-' + calId);
20781d05cddcSAtari911    const dataElement = document.getElementById('namespaces-data-' + calId);
20791d05cddcSAtari911
20801d05cddcSAtari911    if (!searchInput || !hiddenInput || !dropdown || !dataElement) {
20811d05cddcSAtari911        return; // Elements not found
20821d05cddcSAtari911    }
20831d05cddcSAtari911
20841d05cddcSAtari911    let namespaces = [];
20851d05cddcSAtari911    try {
20861d05cddcSAtari911        namespaces = JSON.parse(dataElement.textContent);
20871d05cddcSAtari911    } catch (e) {
20881d05cddcSAtari911        console.error('Failed to parse namespaces data:', e);
20891d05cddcSAtari911        return;
20901d05cddcSAtari911    }
20911d05cddcSAtari911
20921d05cddcSAtari911    let selectedIndex = -1;
20931d05cddcSAtari911
20941d05cddcSAtari911    // Filter and show dropdown
20951d05cddcSAtari911    function filterNamespaces(query) {
20961d05cddcSAtari911        if (!query || query.trim() === '') {
20971d05cddcSAtari911            // Show all namespaces when empty
20981d05cddcSAtari911            hiddenInput.value = '';
20991d05cddcSAtari911            const results = namespaces.slice(0, 20); // Limit to 20
21001d05cddcSAtari911            showDropdown(results);
21011d05cddcSAtari911            return;
21021d05cddcSAtari911        }
21031d05cddcSAtari911
21041d05cddcSAtari911        // Fuzzy match and score
21051d05cddcSAtari911        const matches = [];
21061d05cddcSAtari911        for (let i = 0; i < namespaces.length; i++) {
21071d05cddcSAtari911            const score = fuzzyMatch(query, namespaces[i]);
21081d05cddcSAtari911            if (score !== null) {
21091d05cddcSAtari911                matches.push({ namespace: namespaces[i], score: score });
21101d05cddcSAtari911            }
21111d05cddcSAtari911        }
21121d05cddcSAtari911
21131d05cddcSAtari911        // Sort by score (descending)
21141d05cddcSAtari911        matches.sort((a, b) => b.score - a.score);
21151d05cddcSAtari911
21161d05cddcSAtari911        // Take top 20 results
21171d05cddcSAtari911        const results = matches.slice(0, 20).map(m => m.namespace);
21181d05cddcSAtari911        showDropdown(results);
21191d05cddcSAtari911    }
21201d05cddcSAtari911
21211d05cddcSAtari911    function showDropdown(results) {
21221d05cddcSAtari911        dropdown.innerHTML = '';
21231d05cddcSAtari911        selectedIndex = -1;
21241d05cddcSAtari911
21251d05cddcSAtari911        if (results.length === 0) {
21261d05cddcSAtari911            dropdown.style.display = 'none';
21271d05cddcSAtari911            return;
21281d05cddcSAtari911        }
21291d05cddcSAtari911
21301d05cddcSAtari911        // Add (default) option
21311d05cddcSAtari911        const defaultOption = document.createElement('div');
21321d05cddcSAtari911        defaultOption.className = 'namespace-option';
21331d05cddcSAtari911        defaultOption.textContent = '(default)';
21341d05cddcSAtari911        defaultOption.dataset.value = '';
21351d05cddcSAtari911        dropdown.appendChild(defaultOption);
21361d05cddcSAtari911
21371d05cddcSAtari911        results.forEach(ns => {
21381d05cddcSAtari911            const option = document.createElement('div');
21391d05cddcSAtari911            option.className = 'namespace-option';
21401d05cddcSAtari911            option.textContent = ns;
21411d05cddcSAtari911            option.dataset.value = ns;
21421d05cddcSAtari911            dropdown.appendChild(option);
21431d05cddcSAtari911        });
21441d05cddcSAtari911
21451d05cddcSAtari911        dropdown.style.display = 'block';
21461d05cddcSAtari911    }
21471d05cddcSAtari911
21481d05cddcSAtari911    function hideDropdown() {
21491d05cddcSAtari911        dropdown.style.display = 'none';
21501d05cddcSAtari911        selectedIndex = -1;
21511d05cddcSAtari911    }
21521d05cddcSAtari911
21531d05cddcSAtari911    function selectOption(namespace) {
21541d05cddcSAtari911        hiddenInput.value = namespace;
21551d05cddcSAtari911        searchInput.value = namespace || '(default)';
21561d05cddcSAtari911        hideDropdown();
21571d05cddcSAtari911    }
21581d05cddcSAtari911
21591d05cddcSAtari911    // Event listeners
21601d05cddcSAtari911    searchInput.addEventListener('input', function(e) {
21611d05cddcSAtari911        filterNamespaces(e.target.value);
21621d05cddcSAtari911    });
21631d05cddcSAtari911
21641d05cddcSAtari911    searchInput.addEventListener('focus', function(e) {
21651d05cddcSAtari911        filterNamespaces(e.target.value);
21661d05cddcSAtari911    });
21671d05cddcSAtari911
21681d05cddcSAtari911    searchInput.addEventListener('blur', function(e) {
21691d05cddcSAtari911        // Delay to allow click on dropdown
21701d05cddcSAtari911        setTimeout(hideDropdown, 200);
21711d05cddcSAtari911    });
21721d05cddcSAtari911
21731d05cddcSAtari911    searchInput.addEventListener('keydown', function(e) {
21741d05cddcSAtari911        const options = dropdown.querySelectorAll('.namespace-option');
21751d05cddcSAtari911
21761d05cddcSAtari911        if (e.key === 'ArrowDown') {
21771d05cddcSAtari911            e.preventDefault();
21781d05cddcSAtari911            selectedIndex = Math.min(selectedIndex + 1, options.length - 1);
21791d05cddcSAtari911            updateSelection(options);
21801d05cddcSAtari911        } else if (e.key === 'ArrowUp') {
21811d05cddcSAtari911            e.preventDefault();
21821d05cddcSAtari911            selectedIndex = Math.max(selectedIndex - 1, -1);
21831d05cddcSAtari911            updateSelection(options);
21841d05cddcSAtari911        } else if (e.key === 'Enter') {
21851d05cddcSAtari911            e.preventDefault();
21861d05cddcSAtari911            if (selectedIndex >= 0 && options[selectedIndex]) {
21871d05cddcSAtari911                selectOption(options[selectedIndex].dataset.value);
21881d05cddcSAtari911            }
21891d05cddcSAtari911        } else if (e.key === 'Escape') {
21901d05cddcSAtari911            hideDropdown();
21911d05cddcSAtari911        }
21921d05cddcSAtari911    });
21931d05cddcSAtari911
21941d05cddcSAtari911    function updateSelection(options) {
21951d05cddcSAtari911        options.forEach((opt, idx) => {
21961d05cddcSAtari911            if (idx === selectedIndex) {
21971d05cddcSAtari911                opt.classList.add('selected');
21981d05cddcSAtari911                opt.scrollIntoView({ block: 'nearest' });
21991d05cddcSAtari911            } else {
22001d05cddcSAtari911                opt.classList.remove('selected');
22011d05cddcSAtari911            }
22021d05cddcSAtari911        });
22031d05cddcSAtari911    }
22041d05cddcSAtari911
22051d05cddcSAtari911    // Click on dropdown option
22061d05cddcSAtari911    dropdown.addEventListener('mousedown', function(e) {
22071d05cddcSAtari911        if (e.target.classList.contains('namespace-option')) {
22081d05cddcSAtari911            selectOption(e.target.dataset.value);
22091d05cddcSAtari911        }
22101d05cddcSAtari911    });
22111d05cddcSAtari911};
22121d05cddcSAtari911
22131d05cddcSAtari911// Update end time options based on start time selection
22141d05cddcSAtari911window.updateEndTimeOptions = function(calId) {
22151d05cddcSAtari911    const startTimeSelect = document.getElementById('event-time-' + calId);
22161d05cddcSAtari911    const endTimeSelect = document.getElementById('event-end-time-' + calId);
22171d05cddcSAtari911
22181d05cddcSAtari911    if (!startTimeSelect || !endTimeSelect) return;
22191d05cddcSAtari911
22201d05cddcSAtari911    const startTime = startTimeSelect.value;
22211d05cddcSAtari911
22221d05cddcSAtari911    // If start time is empty (all day), disable end time
22231d05cddcSAtari911    if (!startTime) {
22241d05cddcSAtari911        endTimeSelect.disabled = true;
22251d05cddcSAtari911        endTimeSelect.value = '';
22261d05cddcSAtari911        return;
22271d05cddcSAtari911    }
22281d05cddcSAtari911
22291d05cddcSAtari911    // Enable end time select
22301d05cddcSAtari911    endTimeSelect.disabled = false;
22311d05cddcSAtari911
22321d05cddcSAtari911    // Convert start time to minutes
22331d05cddcSAtari911    const startMinutes = timeToMinutes(startTime);
22341d05cddcSAtari911
22351d05cddcSAtari911    // Get current end time value (to preserve if valid)
22361d05cddcSAtari911    const currentEndTime = endTimeSelect.value;
22371d05cddcSAtari911    const currentEndMinutes = currentEndTime ? timeToMinutes(currentEndTime) : 0;
22381d05cddcSAtari911
22391d05cddcSAtari911    // Filter options - show only times after start time
22401d05cddcSAtari911    const options = endTimeSelect.options;
22411d05cddcSAtari911    let firstValidOption = null;
22421d05cddcSAtari911    let currentStillValid = false;
22431d05cddcSAtari911
22441d05cddcSAtari911    for (let i = 0; i < options.length; i++) {
22451d05cddcSAtari911        const option = options[i];
22461d05cddcSAtari911        const optionValue = option.value;
22471d05cddcSAtari911
22481d05cddcSAtari911        if (optionValue === '') {
22491d05cddcSAtari911            // Keep "Same as start" option visible
22501d05cddcSAtari911            option.style.display = '';
22511d05cddcSAtari911            continue;
22521d05cddcSAtari911        }
22531d05cddcSAtari911
22541d05cddcSAtari911        const optionMinutes = timeToMinutes(optionValue);
22551d05cddcSAtari911
22561d05cddcSAtari911        if (optionMinutes > startMinutes) {
22571d05cddcSAtari911            // Show options after start time
22581d05cddcSAtari911            option.style.display = '';
22591d05cddcSAtari911            if (!firstValidOption) {
22601d05cddcSAtari911                firstValidOption = optionValue;
22611d05cddcSAtari911            }
22621d05cddcSAtari911            if (optionValue === currentEndTime) {
22631d05cddcSAtari911                currentStillValid = true;
22641d05cddcSAtari911            }
22651d05cddcSAtari911        } else {
22661d05cddcSAtari911            // Hide options before or equal to start time
22671d05cddcSAtari911            option.style.display = 'none';
22681d05cddcSAtari911        }
22691d05cddcSAtari911    }
22701d05cddcSAtari911
22711d05cddcSAtari911    // If current end time is now invalid, set a new one
22721d05cddcSAtari911    if (!currentStillValid || currentEndMinutes <= startMinutes) {
22731d05cddcSAtari911        // Try to set to 1 hour after start
22741d05cddcSAtari911        const [startHour, startMinute] = startTime.split(':').map(Number);
22751d05cddcSAtari911        let endHour = startHour + 1;
22761d05cddcSAtari911        let endMinute = startMinute;
22771d05cddcSAtari911
22781d05cddcSAtari911        if (endHour >= 24) {
22791d05cddcSAtari911            endHour = 23;
22801d05cddcSAtari911            endMinute = 45;
22811d05cddcSAtari911        }
22821d05cddcSAtari911
22831d05cddcSAtari911        const suggestedEndTime = String(endHour).padStart(2, '0') + ':' + String(endMinute).padStart(2, '0');
22841d05cddcSAtari911
22851d05cddcSAtari911        // Check if suggested time is in the list
22861d05cddcSAtari911        const suggestedExists = Array.from(options).some(opt => opt.value === suggestedEndTime);
22871d05cddcSAtari911
22881d05cddcSAtari911        if (suggestedExists) {
22891d05cddcSAtari911            endTimeSelect.value = suggestedEndTime;
22901d05cddcSAtari911        } else if (firstValidOption) {
22911d05cddcSAtari911            // Use first valid option
22921d05cddcSAtari911            endTimeSelect.value = firstValidOption;
22931d05cddcSAtari911        } else {
22941d05cddcSAtari911            // No valid options (shouldn't happen, but just in case)
22951d05cddcSAtari911            endTimeSelect.value = '';
22961d05cddcSAtari911        }
22971d05cddcSAtari911    }
22981d05cddcSAtari911};
22991d05cddcSAtari911
23001d05cddcSAtari911// Check for time conflicts between events on the same date
23011d05cddcSAtari911window.checkTimeConflicts = function(events, currentEventId) {
23021d05cddcSAtari911    const conflicts = [];
23031d05cddcSAtari911
23041d05cddcSAtari911    // Group events by date
23051d05cddcSAtari911    const eventsByDate = {};
23061d05cddcSAtari911    for (const [date, dateEvents] of Object.entries(events)) {
23071d05cddcSAtari911        if (!Array.isArray(dateEvents)) continue;
23081d05cddcSAtari911
23091d05cddcSAtari911        dateEvents.forEach(evt => {
23101d05cddcSAtari911            if (!evt.time || evt.id === currentEventId) return; // Skip all-day events and current event
23111d05cddcSAtari911
23121d05cddcSAtari911            if (!eventsByDate[date]) eventsByDate[date] = [];
23131d05cddcSAtari911            eventsByDate[date].push(evt);
23141d05cddcSAtari911        });
23151d05cddcSAtari911    }
23161d05cddcSAtari911
23171d05cddcSAtari911    // Check for overlaps on each date
23181d05cddcSAtari911    for (const [date, dateEvents] of Object.entries(eventsByDate)) {
23191d05cddcSAtari911        for (let i = 0; i < dateEvents.length; i++) {
23201d05cddcSAtari911            for (let j = i + 1; j < dateEvents.length; j++) {
23211d05cddcSAtari911                const evt1 = dateEvents[i];
23221d05cddcSAtari911                const evt2 = dateEvents[j];
23231d05cddcSAtari911
23241d05cddcSAtari911                if (eventsOverlap(evt1, evt2)) {
23251d05cddcSAtari911                    // Mark both events as conflicting
23261d05cddcSAtari911                    if (!evt1.hasConflict) evt1.hasConflict = true;
23271d05cddcSAtari911                    if (!evt2.hasConflict) evt2.hasConflict = true;
23281d05cddcSAtari911
23291d05cddcSAtari911                    // Store conflict info
23301d05cddcSAtari911                    if (!evt1.conflictsWith) evt1.conflictsWith = [];
23311d05cddcSAtari911                    if (!evt2.conflictsWith) evt2.conflictsWith = [];
23321d05cddcSAtari911
23331d05cddcSAtari911                    evt1.conflictsWith.push({id: evt2.id, title: evt2.title, time: evt2.time, endTime: evt2.endTime});
23341d05cddcSAtari911                    evt2.conflictsWith.push({id: evt1.id, title: evt1.title, time: evt1.time, endTime: evt1.endTime});
23351d05cddcSAtari911                }
23361d05cddcSAtari911            }
23371d05cddcSAtari911        }
23381d05cddcSAtari911    }
23391d05cddcSAtari911
23401d05cddcSAtari911    return events;
23411d05cddcSAtari911};
23421d05cddcSAtari911
23431d05cddcSAtari911// Check if two events overlap in time
23441d05cddcSAtari911function eventsOverlap(evt1, evt2) {
23451d05cddcSAtari911    if (!evt1.time || !evt2.time) return false; // All-day events don't conflict
23461d05cddcSAtari911
23471d05cddcSAtari911    const start1 = evt1.time;
23481d05cddcSAtari911    const end1 = evt1.endTime || evt1.time; // If no end time, treat as same as start
23491d05cddcSAtari911
23501d05cddcSAtari911    const start2 = evt2.time;
23511d05cddcSAtari911    const end2 = evt2.endTime || evt2.time;
23521d05cddcSAtari911
23531d05cddcSAtari911    // Convert to minutes for easier comparison
23541d05cddcSAtari911    const start1Mins = timeToMinutes(start1);
23551d05cddcSAtari911    const end1Mins = timeToMinutes(end1);
23561d05cddcSAtari911    const start2Mins = timeToMinutes(start2);
23571d05cddcSAtari911    const end2Mins = timeToMinutes(end2);
23581d05cddcSAtari911
23591d05cddcSAtari911    // Check for overlap
23601d05cddcSAtari911    // Events overlap if: start1 < end2 AND start2 < end1
23611d05cddcSAtari911    return start1Mins < end2Mins && start2Mins < end1Mins;
23621d05cddcSAtari911}
23631d05cddcSAtari911
23641d05cddcSAtari911// Convert HH:MM time to minutes since midnight
23651d05cddcSAtari911function timeToMinutes(timeStr) {
23661d05cddcSAtari911    const [hours, minutes] = timeStr.split(':').map(Number);
23671d05cddcSAtari911    return hours * 60 + minutes;
23681d05cddcSAtari911}
23691d05cddcSAtari911
23701d05cddcSAtari911// Format time range for display
23711d05cddcSAtari911window.formatTimeRange = function(startTime, endTime) {
23721d05cddcSAtari911    if (!startTime) return '';
23731d05cddcSAtari911
23741d05cddcSAtari911    const formatTime = (timeStr) => {
23751d05cddcSAtari911        const [hour24, minute] = timeStr.split(':').map(Number);
23761d05cddcSAtari911        const hour12 = hour24 === 0 ? 12 : (hour24 > 12 ? hour24 - 12 : hour24);
23771d05cddcSAtari911        const ampm = hour24 < 12 ? 'AM' : 'PM';
23781d05cddcSAtari911        return hour12 + ':' + String(minute).padStart(2, '0') + ' ' + ampm;
23791d05cddcSAtari911    };
23801d05cddcSAtari911
23811d05cddcSAtari911    if (!endTime || endTime === startTime) {
23821d05cddcSAtari911        return formatTime(startTime);
23831d05cddcSAtari911    }
23841d05cddcSAtari911
23851d05cddcSAtari911    return formatTime(startTime) + ' - ' + formatTime(endTime);
23861d05cddcSAtari911};
23871d05cddcSAtari911
2388*9ccd446eSAtari911// Track last known mouse position for tooltip positioning fallback
2389*9ccd446eSAtari911var _lastMouseX = 0, _lastMouseY = 0;
2390*9ccd446eSAtari911document.addEventListener('mousemove', function(e) {
2391*9ccd446eSAtari911    _lastMouseX = e.clientX;
2392*9ccd446eSAtari911    _lastMouseY = e.clientY;
2393*9ccd446eSAtari911});
2394*9ccd446eSAtari911
23951d05cddcSAtari911// Show custom conflict tooltip
23961d05cddcSAtari911window.showConflictTooltip = function(badgeElement) {
23971d05cddcSAtari911    // Remove any existing tooltip
23981d05cddcSAtari911    hideConflictTooltip();
23991d05cddcSAtari911
2400*9ccd446eSAtari911    // Get conflict data (base64-encoded JSON to avoid attribute quote issues)
2401*9ccd446eSAtari911    const conflictsRaw = badgeElement.getAttribute('data-conflicts');
2402*9ccd446eSAtari911    if (!conflictsRaw) return;
24031d05cddcSAtari911
24041d05cddcSAtari911    let conflicts;
24051d05cddcSAtari911    try {
2406*9ccd446eSAtari911        conflicts = JSON.parse(decodeURIComponent(escape(atob(conflictsRaw))));
24071d05cddcSAtari911    } catch (e) {
2408*9ccd446eSAtari911        // Fallback: try parsing as plain JSON (for PHP-rendered badges)
2409*9ccd446eSAtari911        try {
2410*9ccd446eSAtari911            conflicts = JSON.parse(conflictsRaw);
2411*9ccd446eSAtari911        } catch (e2) {
2412*9ccd446eSAtari911            console.error('Failed to parse conflicts:', e2);
24131d05cddcSAtari911            return;
24141d05cddcSAtari911        }
2415*9ccd446eSAtari911    }
2416*9ccd446eSAtari911
2417*9ccd446eSAtari911    // Get theme from the calendar container via CSS variables
2418*9ccd446eSAtari911    // Try closest ancestor first, then fall back to any calendar on the page
2419*9ccd446eSAtari911    let containerEl = badgeElement.closest('[id^="cal_"], [id^="panel_"], [id^="sidebar-widget-"], .calendar-compact-container, .event-panel-standalone');
2420*9ccd446eSAtari911    if (!containerEl) {
2421*9ccd446eSAtari911        // Badge might be inside a day popup (appended to body) - find any calendar container
2422*9ccd446eSAtari911        containerEl = document.querySelector('.calendar-compact-container, .event-panel-standalone, [id^="sidebar-widget-"]');
2423*9ccd446eSAtari911    }
2424*9ccd446eSAtari911    const cs = containerEl ? getComputedStyle(containerEl) : null;
2425*9ccd446eSAtari911
2426*9ccd446eSAtari911    const bg = cs ? cs.getPropertyValue('--background-site').trim() || '#242424' : '#242424';
2427*9ccd446eSAtari911    const border = cs ? cs.getPropertyValue('--border-main').trim() || '#00cc07' : '#00cc07';
2428*9ccd446eSAtari911    const textPrimary = cs ? cs.getPropertyValue('--text-primary').trim() || '#00cc07' : '#00cc07';
2429*9ccd446eSAtari911    const textDim = cs ? cs.getPropertyValue('--text-dim').trim() || '#00aa00' : '#00aa00';
2430*9ccd446eSAtari911    const shadow = cs ? cs.getPropertyValue('--shadow-color').trim() || 'rgba(0, 204, 7, 0.3)' : 'rgba(0, 204, 7, 0.3)';
24311d05cddcSAtari911
24321d05cddcSAtari911    // Create tooltip
24331d05cddcSAtari911    const tooltip = document.createElement('div');
24341d05cddcSAtari911    tooltip.id = 'conflict-tooltip';
24351d05cddcSAtari911    tooltip.className = 'conflict-tooltip';
24361d05cddcSAtari911
2437*9ccd446eSAtari911    // Apply theme styles
2438*9ccd446eSAtari911    tooltip.style.background = bg;
2439*9ccd446eSAtari911    tooltip.style.borderColor = border;
2440*9ccd446eSAtari911    tooltip.style.color = textPrimary;
2441*9ccd446eSAtari911    tooltip.style.boxShadow = '0 4px 12px ' + shadow;
2442*9ccd446eSAtari911
2443*9ccd446eSAtari911    // Build content with themed colors
2444*9ccd446eSAtari911    let html = '<div class="conflict-tooltip-header" style="color: ' + textPrimary + '; border-bottom: 1px solid ' + border + ';">⚠️ Time Conflicts</div>';
24451d05cddcSAtari911    html += '<div class="conflict-tooltip-body">';
24461d05cddcSAtari911    conflicts.forEach(conflict => {
2447*9ccd446eSAtari911        html += '<div class="conflict-item" style="color: ' + textDim + ';">• ' + escapeHtml(conflict) + '</div>';
24481d05cddcSAtari911    });
24491d05cddcSAtari911    html += '</div>';
24501d05cddcSAtari911
24511d05cddcSAtari911    tooltip.innerHTML = html;
24521d05cddcSAtari911    document.body.appendChild(tooltip);
24531d05cddcSAtari911
24541d05cddcSAtari911    // Position tooltip
24551d05cddcSAtari911    const rect = badgeElement.getBoundingClientRect();
24561d05cddcSAtari911    const tooltipRect = tooltip.getBoundingClientRect();
24571d05cddcSAtari911
24581d05cddcSAtari911    // Position above the badge, centered
24591d05cddcSAtari911    let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
24601d05cddcSAtari911    let top = rect.top - tooltipRect.height - 8;
24611d05cddcSAtari911
24621d05cddcSAtari911    // Keep tooltip within viewport
24631d05cddcSAtari911    if (left < 10) left = 10;
24641d05cddcSAtari911    if (left + tooltipRect.width > window.innerWidth - 10) {
24651d05cddcSAtari911        left = window.innerWidth - tooltipRect.width - 10;
24661d05cddcSAtari911    }
24671d05cddcSAtari911    if (top < 10) {
24681d05cddcSAtari911        // If not enough room above, show below
24691d05cddcSAtari911        top = rect.bottom + 8;
24701d05cddcSAtari911    }
24711d05cddcSAtari911
24721d05cddcSAtari911    tooltip.style.left = left + 'px';
24731d05cddcSAtari911    tooltip.style.top = top + 'px';
24741d05cddcSAtari911    tooltip.style.opacity = '1';
24751d05cddcSAtari911};
24761d05cddcSAtari911
24771d05cddcSAtari911// Hide conflict tooltip
24781d05cddcSAtari911window.hideConflictTooltip = function() {
24791d05cddcSAtari911    const tooltip = document.getElementById('conflict-tooltip');
24801d05cddcSAtari911    if (tooltip) {
24811d05cddcSAtari911        tooltip.remove();
24821d05cddcSAtari911    }
24831d05cddcSAtari911};
24841d05cddcSAtari911
24851d05cddcSAtari911// Filter events by search term
24861d05cddcSAtari911window.filterEvents = function(calId, searchTerm) {
24871d05cddcSAtari911    const eventList = document.getElementById('eventlist-' + calId);
24881d05cddcSAtari911    const searchClear = document.getElementById('search-clear-' + calId);
24891d05cddcSAtari911
24901d05cddcSAtari911    if (!eventList) return;
24911d05cddcSAtari911
24921d05cddcSAtari911    // Show/hide clear button
24931d05cddcSAtari911    if (searchClear) {
24941d05cddcSAtari911        searchClear.style.display = searchTerm ? 'block' : 'none';
24951d05cddcSAtari911    }
24961d05cddcSAtari911
24971d05cddcSAtari911    searchTerm = searchTerm.toLowerCase().trim();
24981d05cddcSAtari911
24991d05cddcSAtari911    // Get all event items
25001d05cddcSAtari911    const eventItems = eventList.querySelectorAll('.event-compact-item');
25011d05cddcSAtari911    let visibleCount = 0;
25021d05cddcSAtari911    let hiddenPastCount = 0;
25031d05cddcSAtari911
25041d05cddcSAtari911    eventItems.forEach(item => {
25051d05cddcSAtari911        const title = item.querySelector('.event-title-compact');
25061d05cddcSAtari911        const description = item.querySelector('.event-desc-compact');
25071d05cddcSAtari911        const dateTime = item.querySelector('.event-date-time');
25081d05cddcSAtari911
25091d05cddcSAtari911        // Build searchable text
25101d05cddcSAtari911        let searchableText = '';
25111d05cddcSAtari911        if (title) searchableText += title.textContent.toLowerCase() + ' ';
25121d05cddcSAtari911        if (description) searchableText += description.textContent.toLowerCase() + ' ';
25131d05cddcSAtari911        if (dateTime) searchableText += dateTime.textContent.toLowerCase() + ' ';
25141d05cddcSAtari911
25151d05cddcSAtari911        // Check if matches search
25161d05cddcSAtari911        const matches = !searchTerm || searchableText.includes(searchTerm);
25171d05cddcSAtari911
25181d05cddcSAtari911        if (matches) {
25191d05cddcSAtari911            item.style.display = '';
25201d05cddcSAtari911            visibleCount++;
25211d05cddcSAtari911        } else {
25221d05cddcSAtari911            item.style.display = 'none';
25231d05cddcSAtari911            // Check if this is a past event
25241d05cddcSAtari911            if (item.classList.contains('event-past') || item.classList.contains('event-completed')) {
25251d05cddcSAtari911                hiddenPastCount++;
25261d05cddcSAtari911            }
25271d05cddcSAtari911        }
25281d05cddcSAtari911    });
25291d05cddcSAtari911
25301d05cddcSAtari911    // Update past events toggle if it exists
25311d05cddcSAtari911    const pastToggle = eventList.querySelector('.past-events-toggle');
25321d05cddcSAtari911    const pastLabel = eventList.querySelector('.past-events-label');
25331d05cddcSAtari911    const pastContent = document.getElementById('past-events-' + calId);
25341d05cddcSAtari911
25351d05cddcSAtari911    if (pastToggle && pastLabel && pastContent) {
25361d05cddcSAtari911        const visiblePastEvents = pastContent.querySelectorAll('.event-compact-item:not([style*="display: none"])');
25371d05cddcSAtari911        const totalPastVisible = visiblePastEvents.length;
25381d05cddcSAtari911
25391d05cddcSAtari911        if (totalPastVisible > 0) {
25401d05cddcSAtari911            pastLabel.textContent = `Past Events (${totalPastVisible})`;
25411d05cddcSAtari911            pastToggle.style.display = '';
25421d05cddcSAtari911        } else {
25431d05cddcSAtari911            pastToggle.style.display = 'none';
25441d05cddcSAtari911        }
25451d05cddcSAtari911    }
25461d05cddcSAtari911
25471d05cddcSAtari911    // Show "no results" message if nothing visible
25481d05cddcSAtari911    let noResultsMsg = eventList.querySelector('.no-search-results');
25491d05cddcSAtari911    if (visibleCount === 0 && searchTerm) {
25501d05cddcSAtari911        if (!noResultsMsg) {
25511d05cddcSAtari911            noResultsMsg = document.createElement('p');
25521d05cddcSAtari911            noResultsMsg.className = 'no-search-results no-events-msg';
25531d05cddcSAtari911            noResultsMsg.textContent = 'No events match your search';
25541d05cddcSAtari911            eventList.appendChild(noResultsMsg);
25551d05cddcSAtari911        }
25561d05cddcSAtari911        noResultsMsg.style.display = 'block';
25571d05cddcSAtari911    } else if (noResultsMsg) {
25581d05cddcSAtari911        noResultsMsg.style.display = 'none';
25591d05cddcSAtari911    }
25601d05cddcSAtari911};
25611d05cddcSAtari911
25621d05cddcSAtari911// Clear event search
25631d05cddcSAtari911window.clearEventSearch = function(calId) {
25641d05cddcSAtari911    const searchInput = document.getElementById('event-search-' + calId);
25651d05cddcSAtari911    if (searchInput) {
25661d05cddcSAtari911        searchInput.value = '';
25671d05cddcSAtari911        filterEvents(calId, '');
25681d05cddcSAtari911        searchInput.focus();
25691d05cddcSAtari911    }
25701d05cddcSAtari911};
25711d05cddcSAtari911
2572*9ccd446eSAtari911// ============================================
2573*9ccd446eSAtari911// PINK THEME - GLOWING PARTICLE EFFECTS
2574*9ccd446eSAtari911// ============================================
2575*9ccd446eSAtari911
2576*9ccd446eSAtari911// Create glowing pink particle effects for pink theme
2577*9ccd446eSAtari911(function() {
2578*9ccd446eSAtari911    let pinkThemeActive = false;
2579*9ccd446eSAtari911    let trailTimer = null;
2580*9ccd446eSAtari911    let pixelTimer = null;
2581*9ccd446eSAtari911
2582*9ccd446eSAtari911    // Check if pink theme is active
2583*9ccd446eSAtari911    function checkPinkTheme() {
2584*9ccd446eSAtari911        const pinkCalendars = document.querySelectorAll('.calendar-theme-pink');
2585*9ccd446eSAtari911        pinkThemeActive = pinkCalendars.length > 0;
2586*9ccd446eSAtari911        return pinkThemeActive;
2587*9ccd446eSAtari911    }
2588*9ccd446eSAtari911
2589*9ccd446eSAtari911    // Create trail particle
2590*9ccd446eSAtari911    function createTrailParticle(clientX, clientY) {
2591*9ccd446eSAtari911        if (!pinkThemeActive) return;
2592*9ccd446eSAtari911
2593*9ccd446eSAtari911        const trail = document.createElement('div');
2594*9ccd446eSAtari911        trail.className = 'pink-cursor-trail';
2595*9ccd446eSAtari911        trail.style.left = clientX + 'px';
2596*9ccd446eSAtari911        trail.style.top = clientY + 'px';
2597*9ccd446eSAtari911        trail.style.animation = 'cursor-trail-fade 0.5s ease-out forwards';
2598*9ccd446eSAtari911
2599*9ccd446eSAtari911        document.body.appendChild(trail);
2600*9ccd446eSAtari911
2601*9ccd446eSAtari911        setTimeout(function() {
2602*9ccd446eSAtari911            trail.remove();
2603*9ccd446eSAtari911        }, 500);
2604*9ccd446eSAtari911    }
2605*9ccd446eSAtari911
2606*9ccd446eSAtari911    // Create pixel sparkles
2607*9ccd446eSAtari911    function createPixelSparkles(clientX, clientY) {
2608*9ccd446eSAtari911        if (!pinkThemeActive || pixelTimer) return;
2609*9ccd446eSAtari911
2610*9ccd446eSAtari911        const pixelCount = 3 + Math.floor(Math.random() * 4); // 3-6 pixels
2611*9ccd446eSAtari911
2612*9ccd446eSAtari911        for (let i = 0; i < pixelCount; i++) {
2613*9ccd446eSAtari911            const pixel = document.createElement('div');
2614*9ccd446eSAtari911            pixel.className = 'pink-pixel-sparkle';
2615*9ccd446eSAtari911
2616*9ccd446eSAtari911            // Random offset from cursor
2617*9ccd446eSAtari911            const offsetX = (Math.random() - 0.5) * 30;
2618*9ccd446eSAtari911            const offsetY = (Math.random() - 0.5) * 30;
2619*9ccd446eSAtari911
2620*9ccd446eSAtari911            pixel.style.left = (clientX + offsetX) + 'px';
2621*9ccd446eSAtari911            pixel.style.top = (clientY + offsetY) + 'px';
2622*9ccd446eSAtari911
2623*9ccd446eSAtari911            // Random color - bright neon pinks and whites
2624*9ccd446eSAtari911            const colors = ['#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1'];
2625*9ccd446eSAtari911            const color = colors[Math.floor(Math.random() * colors.length)];
2626*9ccd446eSAtari911            pixel.style.background = color;
2627*9ccd446eSAtari911            pixel.style.boxShadow = '0 0 2px ' + color + ', 0 0 4px ' + color + ', 0 0 6px #fff';
2628*9ccd446eSAtari911
2629*9ccd446eSAtari911            // Random animation
2630*9ccd446eSAtari911            if (Math.random() > 0.5) {
2631*9ccd446eSAtari911                pixel.style.animation = 'pixel-twinkle 0.6s ease-out forwards';
2632*9ccd446eSAtari911            } else {
2633*9ccd446eSAtari911                pixel.style.animation = 'pixel-float-away 0.8s ease-out forwards';
2634*9ccd446eSAtari911            }
2635*9ccd446eSAtari911
2636*9ccd446eSAtari911            document.body.appendChild(pixel);
2637*9ccd446eSAtari911
2638*9ccd446eSAtari911            setTimeout(function() {
2639*9ccd446eSAtari911                pixel.remove();
2640*9ccd446eSAtari911            }, 800);
2641*9ccd446eSAtari911        }
2642*9ccd446eSAtari911
2643*9ccd446eSAtari911        pixelTimer = setTimeout(function() {
2644*9ccd446eSAtari911            pixelTimer = null;
2645*9ccd446eSAtari911        }, 40);
2646*9ccd446eSAtari911    }
2647*9ccd446eSAtari911
2648*9ccd446eSAtari911    // Create explosion
2649*9ccd446eSAtari911    function createExplosion(clientX, clientY) {
2650*9ccd446eSAtari911        if (!pinkThemeActive) return;
2651*9ccd446eSAtari911
2652*9ccd446eSAtari911        const particleCount = 25;
2653*9ccd446eSAtari911        const colors = ['#ff1493', '#ff69b4', '#ff85c1', '#ffc0cb', '#fff'];
2654*9ccd446eSAtari911
2655*9ccd446eSAtari911        // Add hearts to explosion (8-12 hearts)
2656*9ccd446eSAtari911        const heartCount = 8 + Math.floor(Math.random() * 5);
2657*9ccd446eSAtari911        for (let i = 0; i < heartCount; i++) {
2658*9ccd446eSAtari911            const heart = document.createElement('div');
2659*9ccd446eSAtari911            heart.textContent = '��';
2660*9ccd446eSAtari911            heart.style.position = 'fixed';
2661*9ccd446eSAtari911            heart.style.left = clientX + 'px';
2662*9ccd446eSAtari911            heart.style.top = clientY + 'px';
2663*9ccd446eSAtari911            heart.style.pointerEvents = 'none';
2664*9ccd446eSAtari911            heart.style.zIndex = '9999999';
2665*9ccd446eSAtari911            heart.style.fontSize = (12 + Math.random() * 16) + 'px';
2666*9ccd446eSAtari911
2667*9ccd446eSAtari911            // Random direction
2668*9ccd446eSAtari911            const angle = Math.random() * Math.PI * 2;
2669*9ccd446eSAtari911            const velocity = 60 + Math.random() * 80;
2670*9ccd446eSAtari911            const tx = Math.cos(angle) * velocity;
2671*9ccd446eSAtari911            const ty = Math.sin(angle) * velocity;
2672*9ccd446eSAtari911
2673*9ccd446eSAtari911            heart.style.setProperty('--tx', tx + 'px');
2674*9ccd446eSAtari911            heart.style.setProperty('--ty', ty + 'px');
2675*9ccd446eSAtari911
2676*9ccd446eSAtari911            const duration = 0.8 + Math.random() * 0.4;
2677*9ccd446eSAtari911            heart.style.animation = 'particle-explode ' + duration + 's ease-out forwards';
2678*9ccd446eSAtari911
2679*9ccd446eSAtari911            document.body.appendChild(heart);
2680*9ccd446eSAtari911
2681*9ccd446eSAtari911            setTimeout(function() {
2682*9ccd446eSAtari911                heart.remove();
2683*9ccd446eSAtari911            }, duration * 1000);
2684*9ccd446eSAtari911        }
2685*9ccd446eSAtari911
2686*9ccd446eSAtari911        // Main explosion particles
2687*9ccd446eSAtari911        for (let i = 0; i < particleCount; i++) {
2688*9ccd446eSAtari911            const particle = document.createElement('div');
2689*9ccd446eSAtari911            particle.className = 'pink-particle';
2690*9ccd446eSAtari911
2691*9ccd446eSAtari911            const color = colors[Math.floor(Math.random() * colors.length)];
2692*9ccd446eSAtari911            particle.style.background = 'radial-gradient(circle, ' + color + ', transparent)';
2693*9ccd446eSAtari911            particle.style.boxShadow = '0 0 10px ' + color + ', 0 0 20px ' + color;
2694*9ccd446eSAtari911
2695*9ccd446eSAtari911            particle.style.left = clientX + 'px';
2696*9ccd446eSAtari911            particle.style.top = clientY + 'px';
2697*9ccd446eSAtari911
2698*9ccd446eSAtari911            const angle = (Math.PI * 2 * i) / particleCount;
2699*9ccd446eSAtari911            const velocity = 50 + Math.random() * 100;
2700*9ccd446eSAtari911            const tx = Math.cos(angle) * velocity;
2701*9ccd446eSAtari911            const ty = Math.sin(angle) * velocity;
2702*9ccd446eSAtari911
2703*9ccd446eSAtari911            particle.style.setProperty('--tx', tx + 'px');
2704*9ccd446eSAtari911            particle.style.setProperty('--ty', ty + 'px');
2705*9ccd446eSAtari911
2706*9ccd446eSAtari911            const size = 4 + Math.random() * 6;
2707*9ccd446eSAtari911            particle.style.width = size + 'px';
2708*9ccd446eSAtari911            particle.style.height = size + 'px';
2709*9ccd446eSAtari911
2710*9ccd446eSAtari911            const duration = 0.6 + Math.random() * 0.4;
2711*9ccd446eSAtari911            particle.style.animation = 'particle-explode ' + duration + 's ease-out forwards';
2712*9ccd446eSAtari911
2713*9ccd446eSAtari911            document.body.appendChild(particle);
2714*9ccd446eSAtari911
2715*9ccd446eSAtari911            setTimeout(function() {
2716*9ccd446eSAtari911                particle.remove();
2717*9ccd446eSAtari911            }, duration * 1000);
2718*9ccd446eSAtari911        }
2719*9ccd446eSAtari911
2720*9ccd446eSAtari911        // Pixel sparkles
2721*9ccd446eSAtari911        const pixelSparkleCount = 40;
2722*9ccd446eSAtari911
2723*9ccd446eSAtari911        for (let i = 0; i < pixelSparkleCount; i++) {
2724*9ccd446eSAtari911            const pixel = document.createElement('div');
2725*9ccd446eSAtari911            pixel.className = 'pink-pixel-sparkle';
2726*9ccd446eSAtari911
2727*9ccd446eSAtari911            const pixelColors = ['#fff', '#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1'];
2728*9ccd446eSAtari911            const pixelColor = pixelColors[Math.floor(Math.random() * pixelColors.length)];
2729*9ccd446eSAtari911            pixel.style.background = pixelColor;
2730*9ccd446eSAtari911            pixel.style.boxShadow = '0 0 3px ' + pixelColor + ', 0 0 6px ' + pixelColor + ', 0 0 9px #fff';
2731*9ccd446eSAtari911
2732*9ccd446eSAtari911            const angle = Math.random() * Math.PI * 2;
2733*9ccd446eSAtari911            const distance = 30 + Math.random() * 80;
2734*9ccd446eSAtari911            const offsetX = Math.cos(angle) * distance;
2735*9ccd446eSAtari911            const offsetY = Math.sin(angle) * distance;
2736*9ccd446eSAtari911
2737*9ccd446eSAtari911            pixel.style.left = clientX + 'px';
2738*9ccd446eSAtari911            pixel.style.top = clientY + 'px';
2739*9ccd446eSAtari911            pixel.style.setProperty('--tx', offsetX + 'px');
2740*9ccd446eSAtari911            pixel.style.setProperty('--ty', offsetY + 'px');
2741*9ccd446eSAtari911
2742*9ccd446eSAtari911            const pixelSize = 1 + Math.random() * 2;
2743*9ccd446eSAtari911            pixel.style.width = pixelSize + 'px';
2744*9ccd446eSAtari911            pixel.style.height = pixelSize + 'px';
2745*9ccd446eSAtari911
2746*9ccd446eSAtari911            const duration = 0.4 + Math.random() * 0.4;
2747*9ccd446eSAtari911            if (Math.random() > 0.5) {
2748*9ccd446eSAtari911                pixel.style.animation = 'pixel-twinkle ' + duration + 's ease-out forwards';
2749*9ccd446eSAtari911            } else {
2750*9ccd446eSAtari911                pixel.style.animation = 'particle-explode ' + duration + 's ease-out forwards';
2751*9ccd446eSAtari911            }
2752*9ccd446eSAtari911
2753*9ccd446eSAtari911            document.body.appendChild(pixel);
2754*9ccd446eSAtari911
2755*9ccd446eSAtari911            setTimeout(function() {
2756*9ccd446eSAtari911                pixel.remove();
2757*9ccd446eSAtari911            }, duration * 1000);
2758*9ccd446eSAtari911        }
2759*9ccd446eSAtari911
2760*9ccd446eSAtari911        // Flash
2761*9ccd446eSAtari911        const flash = document.createElement('div');
2762*9ccd446eSAtari911        flash.style.position = 'fixed';
2763*9ccd446eSAtari911        flash.style.left = clientX + 'px';
2764*9ccd446eSAtari911        flash.style.top = clientY + 'px';
2765*9ccd446eSAtari911        flash.style.width = '40px';
2766*9ccd446eSAtari911        flash.style.height = '40px';
2767*9ccd446eSAtari911        flash.style.borderRadius = '50%';
2768*9ccd446eSAtari911        flash.style.background = 'radial-gradient(circle, rgba(255, 255, 255, 0.9), rgba(255, 20, 147, 0.6), transparent)';
2769*9ccd446eSAtari911        flash.style.boxShadow = '0 0 40px #fff, 0 0 60px #ff1493, 0 0 80px #ff69b4';
2770*9ccd446eSAtari911        flash.style.pointerEvents = 'none';
2771*9ccd446eSAtari911        flash.style.zIndex = '9999999';  // Above everything including dialogs
2772*9ccd446eSAtari911        flash.style.transform = 'translate(-50%, -50%)';
2773*9ccd446eSAtari911        flash.style.animation = 'cursor-trail-fade 0.3s ease-out forwards';
2774*9ccd446eSAtari911
2775*9ccd446eSAtari911        document.body.appendChild(flash);
2776*9ccd446eSAtari911
2777*9ccd446eSAtari911        setTimeout(function() {
2778*9ccd446eSAtari911            flash.remove();
2779*9ccd446eSAtari911        }, 300);
2780*9ccd446eSAtari911    }
2781*9ccd446eSAtari911
2782*9ccd446eSAtari911    function initPinkParticles() {
2783*9ccd446eSAtari911        if (!checkPinkTheme()) return;
2784*9ccd446eSAtari911
2785*9ccd446eSAtari911        // Use capture phase to catch events before stopPropagation
2786*9ccd446eSAtari911        document.addEventListener('mousemove', function(e) {
2787*9ccd446eSAtari911            if (!pinkThemeActive) return;
2788*9ccd446eSAtari911
2789*9ccd446eSAtari911            createTrailParticle(e.clientX, e.clientY);
2790*9ccd446eSAtari911            createPixelSparkles(e.clientX, e.clientY);
2791*9ccd446eSAtari911        }, true); // Capture phase!
2792*9ccd446eSAtari911
2793*9ccd446eSAtari911        // Throttle main trail
2794*9ccd446eSAtari911        document.addEventListener('mousemove', function(e) {
2795*9ccd446eSAtari911            if (!pinkThemeActive || trailTimer) return;
2796*9ccd446eSAtari911
2797*9ccd446eSAtari911            trailTimer = setTimeout(function() {
2798*9ccd446eSAtari911                trailTimer = null;
2799*9ccd446eSAtari911            }, 30);
2800*9ccd446eSAtari911        }, true); // Capture phase!
2801*9ccd446eSAtari911
2802*9ccd446eSAtari911        // Click explosion - use capture phase
2803*9ccd446eSAtari911        document.addEventListener('click', function(e) {
2804*9ccd446eSAtari911            if (!pinkThemeActive) return;
2805*9ccd446eSAtari911
2806*9ccd446eSAtari911            createExplosion(e.clientX, e.clientY);
2807*9ccd446eSAtari911        }, true); // Capture phase!
2808*9ccd446eSAtari911    }
2809*9ccd446eSAtari911
2810*9ccd446eSAtari911    // Initialize on load
2811*9ccd446eSAtari911    if (document.readyState === 'loading') {
2812*9ccd446eSAtari911        document.addEventListener('DOMContentLoaded', initPinkParticles);
2813*9ccd446eSAtari911    } else {
2814*9ccd446eSAtari911        initPinkParticles();
2815*9ccd446eSAtari911    }
2816*9ccd446eSAtari911
2817*9ccd446eSAtari911    // Re-check theme if calendar is dynamically added
2818*9ccd446eSAtari911    if (typeof MutationObserver !== 'undefined') {
2819*9ccd446eSAtari911        const observer = new MutationObserver(function(mutations) {
2820*9ccd446eSAtari911            mutations.forEach(function(mutation) {
2821*9ccd446eSAtari911                if (mutation.addedNodes.length > 0) {
2822*9ccd446eSAtari911                    mutation.addedNodes.forEach(function(node) {
2823*9ccd446eSAtari911                        if (node.nodeType === 1 && node.classList && node.classList.contains('calendar-theme-pink')) {
2824*9ccd446eSAtari911                            checkPinkTheme();
2825*9ccd446eSAtari911                            initPinkParticles();
2826*9ccd446eSAtari911                        }
2827*9ccd446eSAtari911                    });
2828*9ccd446eSAtari911                }
2829*9ccd446eSAtari911            });
2830*9ccd446eSAtari911        });
2831*9ccd446eSAtari911
2832*9ccd446eSAtari911        observer.observe(document.body, {
2833*9ccd446eSAtari911            childList: true,
2834*9ccd446eSAtari911            subtree: true
2835*9ccd446eSAtari911        });
2836*9ccd446eSAtari911    }
2837*9ccd446eSAtari911})();
2838*9ccd446eSAtari911
28391d05cddcSAtari911// End of calendar plugin JavaScript
2840