xref: /plugin/calendar/syntax.php (revision 9ccd446ecbe25932c2e89f7608c11495a1f1dbac)
119378907SAtari911<?php
219378907SAtari911/**
319378907SAtari911 * DokuWiki Plugin calendar (Syntax Component)
419378907SAtari911 * Compact design with integrated event list
519378907SAtari911 *
619378907SAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
719378907SAtari911 * @author  DokuWiki Community
819378907SAtari911 */
919378907SAtari911
1019378907SAtari911if (!defined('DOKU_INC')) die();
1119378907SAtari911
1219378907SAtari911class syntax_plugin_calendar extends DokuWiki_Syntax_Plugin {
1319378907SAtari911
1419378907SAtari911    public function getType() {
1519378907SAtari911        return 'substition';
1619378907SAtari911    }
1719378907SAtari911
1819378907SAtari911    public function getPType() {
1919378907SAtari911        return 'block';
2019378907SAtari911    }
2119378907SAtari911
2219378907SAtari911    public function getSort() {
2319378907SAtari911        return 155;
2419378907SAtari911    }
2519378907SAtari911
2619378907SAtari911    public function connectTo($mode) {
2719378907SAtari911        $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
2819378907SAtari911        $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
2919378907SAtari911        $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
3019378907SAtari911    }
3119378907SAtari911
3219378907SAtari911    public function handle($match, $state, $pos, Doku_Handler $handler) {
3319378907SAtari911        $isEventList = (strpos($match, '{{eventlist') === 0);
3419378907SAtari911        $isEventPanel = (strpos($match, '{{eventpanel') === 0);
3519378907SAtari911
3619378907SAtari911        if ($isEventList) {
3719378907SAtari911            $match = substr($match, 12, -2);
3819378907SAtari911        } elseif ($isEventPanel) {
3919378907SAtari911            $match = substr($match, 13, -2);
4019378907SAtari911        } else {
4119378907SAtari911            $match = substr($match, 10, -2);
4219378907SAtari911        }
4319378907SAtari911
4419378907SAtari911        $params = array(
4519378907SAtari911            'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'),
4619378907SAtari911            'year' => date('Y'),
4719378907SAtari911            'month' => date('n'),
4819378907SAtari911            'namespace' => '',
4919378907SAtari911            'daterange' => '',
50e3a9f44cSAtari911            'date' => '',
51e3a9f44cSAtari911            'range' => ''
5219378907SAtari911        );
5319378907SAtari911
5419378907SAtari911        if (trim($match)) {
5519378907SAtari911            $pairs = preg_split('/\s+/', trim($match));
5619378907SAtari911            foreach ($pairs as $pair) {
5719378907SAtari911                if (strpos($pair, '=') !== false) {
5819378907SAtari911                    list($key, $value) = explode('=', $pair, 2);
5919378907SAtari911                    $params[trim($key)] = trim($value);
6087ac9bf3SAtari911                } else {
6187ac9bf3SAtari911                    // Handle standalone flags like "today"
6287ac9bf3SAtari911                    $params[trim($pair)] = true;
6319378907SAtari911                }
6419378907SAtari911            }
6519378907SAtari911        }
6619378907SAtari911
6719378907SAtari911        return $params;
6819378907SAtari911    }
6919378907SAtari911
7019378907SAtari911    public function render($mode, Doku_Renderer $renderer, $data) {
7119378907SAtari911        if ($mode !== 'xhtml') return false;
7219378907SAtari911
7319378907SAtari911        if ($data['type'] === 'eventlist') {
7419378907SAtari911            $html = $this->renderStandaloneEventList($data);
7519378907SAtari911        } elseif ($data['type'] === 'eventpanel') {
7619378907SAtari911            $html = $this->renderEventPanelOnly($data);
7719378907SAtari911        } else {
7819378907SAtari911            $html = $this->renderCompactCalendar($data);
7919378907SAtari911        }
8019378907SAtari911
8119378907SAtari911        $renderer->doc .= $html;
8219378907SAtari911        return true;
8319378907SAtari911    }
8419378907SAtari911
8519378907SAtari911    private function renderCompactCalendar($data) {
8619378907SAtari911        $year = (int)$data['year'];
8719378907SAtari911        $month = (int)$data['month'];
8819378907SAtari911        $namespace = $data['namespace'];
8919378907SAtari911
90*9ccd446eSAtari911        // Get theme
91*9ccd446eSAtari911        $theme = $this->getSidebarTheme();
92*9ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
93*9ccd446eSAtari911        $themeClass = 'calendar-theme-' . $theme;
94*9ccd446eSAtari911
95*9ccd446eSAtari911        // Determine button text color: professional uses white, others use bg color
96*9ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
97*9ccd446eSAtari911
98e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
99e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
100e3a9f44cSAtari911
101e3a9f44cSAtari911        if ($isMultiNamespace) {
102e3a9f44cSAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
103e3a9f44cSAtari911        } else {
10419378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
105e3a9f44cSAtari911        }
10619378907SAtari911        $calId = 'cal_' . md5(serialize($data) . microtime());
10719378907SAtari911
10819378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
10919378907SAtari911
11019378907SAtari911        $prevMonth = $month - 1;
11119378907SAtari911        $prevYear = $year;
11219378907SAtari911        if ($prevMonth < 1) {
11319378907SAtari911            $prevMonth = 12;
11419378907SAtari911            $prevYear--;
11519378907SAtari911        }
11619378907SAtari911
11719378907SAtari911        $nextMonth = $month + 1;
11819378907SAtari911        $nextYear = $year;
11919378907SAtari911        if ($nextMonth > 12) {
12019378907SAtari911            $nextMonth = 1;
12119378907SAtari911            $nextYear++;
12219378907SAtari911        }
12319378907SAtari911
124*9ccd446eSAtari911        // Container - all styling via CSS variables
125*9ccd446eSAtari911        $html = '<div class="calendar-compact-container ' . $themeClass . '" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '" data-year="' . $year . '" data-month="' . $month . '" data-theme="' . $theme . '" data-theme-styles="' . htmlspecialchars(json_encode($themeStyles)) . '">';
126*9ccd446eSAtari911
127*9ccd446eSAtari911        // Inject CSS variables for this calendar instance - all theming flows from here
128*9ccd446eSAtari911        $html .= '<style>
129*9ccd446eSAtari911        #' . $calId . ' {
130*9ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
131*9ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
132*9ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
133*9ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
134*9ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
135*9ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
136*9ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
137*9ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
138*9ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
139*9ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
140*9ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
141*9ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
142*9ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
143*9ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
144*9ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
145*9ccd446eSAtari911        }
146*9ccd446eSAtari911        #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
147*9ccd446eSAtari911        #event-search-' . $calId . '::-webkit-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
148*9ccd446eSAtari911        #event-search-' . $calId . '::-moz-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
149*9ccd446eSAtari911        #event-search-' . $calId . ':-ms-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
150*9ccd446eSAtari911        </style>';
1511d05cddcSAtari911
1521d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
1531d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
1541d05cddcSAtari911
1551d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
1561d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
15719378907SAtari911
15819378907SAtari911        // Embed events data as JSON for JavaScript access
15919378907SAtari911        $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>';
16019378907SAtari911
16119378907SAtari911        // Left side: Calendar
16219378907SAtari911        $html .= '<div class="calendar-compact-left">';
16319378907SAtari911
16419378907SAtari911        // Header with navigation
16519378907SAtari911        $html .= '<div class="calendar-compact-header">';
16619378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
16787ac9bf3SAtari911        $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . '</h3>';
16819378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
16987ac9bf3SAtari911        $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
17019378907SAtari911        $html .= '</div>';
17119378907SAtari911
17219378907SAtari911        // Calendar grid
17319378907SAtari911        $html .= '<table class="calendar-compact-grid">';
17419378907SAtari911        $html .= '<thead><tr>';
17519378907SAtari911        $html .= '<th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th>';
17619378907SAtari911        $html .= '</tr></thead><tbody>';
17719378907SAtari911
17819378907SAtari911        $firstDay = mktime(0, 0, 0, $month, 1, $year);
17919378907SAtari911        $daysInMonth = date('t', $firstDay);
18019378907SAtari911        $dayOfWeek = date('w', $firstDay);
18119378907SAtari911
182e3a9f44cSAtari911        // Build a map of all events with their date ranges for the calendar grid
18387ac9bf3SAtari911        $eventRanges = array();
184e3a9f44cSAtari911        foreach ($events as $dateKey => $dayEvents) {
18587ac9bf3SAtari911            foreach ($dayEvents as $evt) {
18687ac9bf3SAtari911                $eventId = isset($evt['id']) ? $evt['id'] : '';
18787ac9bf3SAtari911                $startDate = $dateKey;
18887ac9bf3SAtari911                $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey;
18987ac9bf3SAtari911
19087ac9bf3SAtari911                // Only process events that touch this month
19187ac9bf3SAtari911                $eventStart = new DateTime($startDate);
19287ac9bf3SAtari911                $eventEnd = new DateTime($endDate);
19387ac9bf3SAtari911                $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month));
19487ac9bf3SAtari911                $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth));
19587ac9bf3SAtari911
19687ac9bf3SAtari911                // Skip if event doesn't overlap with current month
19787ac9bf3SAtari911                if ($eventEnd < $monthStart || $eventStart > $monthEnd) {
19887ac9bf3SAtari911                    continue;
19987ac9bf3SAtari911                }
20087ac9bf3SAtari911
20187ac9bf3SAtari911                // Create entry for each day the event spans
20287ac9bf3SAtari911                $current = clone $eventStart;
20387ac9bf3SAtari911                while ($current <= $eventEnd) {
20487ac9bf3SAtari911                    $currentKey = $current->format('Y-m-d');
20587ac9bf3SAtari911
20687ac9bf3SAtari911                    // Check if this date is in current month
20787ac9bf3SAtari911                    $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey);
20887ac9bf3SAtari911                    if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) {
20987ac9bf3SAtari911                        if (!isset($eventRanges[$currentKey])) {
21087ac9bf3SAtari911                            $eventRanges[$currentKey] = array();
21187ac9bf3SAtari911                        }
21287ac9bf3SAtari911
21387ac9bf3SAtari911                        // Add event with span information
21487ac9bf3SAtari911                        $evt['_span_start'] = $startDate;
21587ac9bf3SAtari911                        $evt['_span_end'] = $endDate;
21687ac9bf3SAtari911                        $evt['_is_first_day'] = ($currentKey === $startDate);
21787ac9bf3SAtari911                        $evt['_is_last_day'] = ($currentKey === $endDate);
21887ac9bf3SAtari911                        $evt['_original_date'] = $dateKey; // Keep track of original date
21987ac9bf3SAtari911
22087ac9bf3SAtari911                        // Check if event continues from previous month or to next month
22187ac9bf3SAtari911                        $evt['_continues_from_prev'] = ($eventStart < $monthStart);
22287ac9bf3SAtari911                        $evt['_continues_to_next'] = ($eventEnd > $monthEnd);
22387ac9bf3SAtari911
22487ac9bf3SAtari911                        $eventRanges[$currentKey][] = $evt;
22587ac9bf3SAtari911                    }
22687ac9bf3SAtari911
22787ac9bf3SAtari911                    $current->modify('+1 day');
22887ac9bf3SAtari911                }
22987ac9bf3SAtari911            }
23087ac9bf3SAtari911        }
23187ac9bf3SAtari911
23219378907SAtari911        $currentDay = 1;
23319378907SAtari911        $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7);
23419378907SAtari911
23519378907SAtari911        for ($row = 0; $row < $rowCount; $row++) {
23619378907SAtari911            $html .= '<tr>';
23719378907SAtari911            for ($col = 0; $col < 7; $col++) {
23819378907SAtari911                if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) {
23919378907SAtari911                    $html .= '<td class="cal-empty"></td>';
24019378907SAtari911                } else {
24119378907SAtari911                    $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay);
24219378907SAtari911                    $isToday = ($dateKey === date('Y-m-d'));
24387ac9bf3SAtari911                    $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]);
24419378907SAtari911
24519378907SAtari911                    $classes = 'cal-day';
24619378907SAtari911                    if ($isToday) $classes .= ' cal-today';
24719378907SAtari911                    if ($hasEvents) $classes .= ' cal-has-events';
24819378907SAtari911
24919378907SAtari911                    $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">';
250*9ccd446eSAtari911
251*9ccd446eSAtari911                    $dayNumClass = $isToday ? 'day-num day-num-today' : 'day-num';
252*9ccd446eSAtari911                    $html .= '<span class="' . $dayNumClass . '">' . $currentDay . '</span>';
25319378907SAtari911
25419378907SAtari911                    if ($hasEvents) {
25519378907SAtari911                        // Sort events by time (no time first, then by time)
25687ac9bf3SAtari911                        $sortedEvents = $eventRanges[$dateKey];
25719378907SAtari911                        usort($sortedEvents, function($a, $b) {
25819378907SAtari911                            $timeA = isset($a['time']) ? $a['time'] : '';
25919378907SAtari911                            $timeB = isset($b['time']) ? $b['time'] : '';
26019378907SAtari911
26119378907SAtari911                            // Events without time go first
26219378907SAtari911                            if (empty($timeA) && !empty($timeB)) return -1;
26319378907SAtari911                            if (!empty($timeA) && empty($timeB)) return 1;
26419378907SAtari911                            if (empty($timeA) && empty($timeB)) return 0;
26519378907SAtari911
26619378907SAtari911                            // Sort by time
26719378907SAtari911                            return strcmp($timeA, $timeB);
26819378907SAtari911                        });
26919378907SAtari911
27019378907SAtari911                        // Show colored stacked bars for each event
27119378907SAtari911                        $html .= '<div class="event-indicators">';
27219378907SAtari911                        foreach ($sortedEvents as $evt) {
27319378907SAtari911                            $eventId = isset($evt['id']) ? $evt['id'] : '';
27419378907SAtari911                            $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db';
27519378907SAtari911                            $eventTime = isset($evt['time']) ? $evt['time'] : '';
27619378907SAtari911                            $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event';
27787ac9bf3SAtari911                            $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey;
27887ac9bf3SAtari911                            $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true;
27987ac9bf3SAtari911                            $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true;
28019378907SAtari911
28119378907SAtari911                            $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed';
28219378907SAtari911
28387ac9bf3SAtari911                            // Add classes for multi-day spanning
28487ac9bf3SAtari911                            if (!$isFirstDay) $barClass .= ' event-bar-continues';
28587ac9bf3SAtari911                            if (!$isLastDay) $barClass .= ' event-bar-continuing';
28687ac9bf3SAtari911
28719378907SAtari911                            $html .= '<span class="event-bar ' . $barClass . '" ';
28819378907SAtari911                            $html .= 'style="background: ' . $eventColor . ';" ';
28919378907SAtari911                            $html .= 'title="' . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" ';
29087ac9bf3SAtari911                            $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">';
29119378907SAtari911                            $html .= '</span>';
29219378907SAtari911                        }
29319378907SAtari911                        $html .= '</div>';
29419378907SAtari911                    }
29519378907SAtari911
29619378907SAtari911                    $html .= '</td>';
29719378907SAtari911                    $currentDay++;
29819378907SAtari911                }
29919378907SAtari911            }
30019378907SAtari911            $html .= '</tr>';
30119378907SAtari911        }
30219378907SAtari911
30319378907SAtari911        $html .= '</tbody></table>';
30419378907SAtari911        $html .= '</div>'; // End calendar-left
30519378907SAtari911
30619378907SAtari911        // Right side: Event list
30719378907SAtari911        $html .= '<div class="calendar-compact-right">';
30819378907SAtari911        $html .= '<div class="event-list-header">';
30919378907SAtari911        $html .= '<div class="event-list-header-content">';
31019378907SAtari911        $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>';
31119378907SAtari911        if ($namespace) {
31219378907SAtari911            $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>';
31319378907SAtari911        }
31419378907SAtari911        $html .= '</div>';
3151d05cddcSAtari911
3161d05cddcSAtari911        // Search bar in header
3171d05cddcSAtari911        $html .= '<div class="event-search-container-inline">';
3181d05cddcSAtari911        $html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" placeholder="�� Search..." oninput="filterEvents(\'' . $calId . '\', this.value)">';
3191d05cddcSAtari911        $html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
3201d05cddcSAtari911        $html .= '</div>';
3211d05cddcSAtari911
32219378907SAtari911        $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
32319378907SAtari911        $html .= '</div>';
32419378907SAtari911
32519378907SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">';
326*9ccd446eSAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace, $themeStyles);
32719378907SAtari911        $html .= '</div>';
32819378907SAtari911
32919378907SAtari911        $html .= '</div>'; // End calendar-right
33019378907SAtari911
33119378907SAtari911        // Event dialog
33219378907SAtari911        $html .= $this->renderEventDialog($calId, $namespace);
33319378907SAtari911
33487ac9bf3SAtari911        // Month/Year picker dialog (at container level for proper overlay)
335*9ccd446eSAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
33687ac9bf3SAtari911
33719378907SAtari911        $html .= '</div>'; // End container
33819378907SAtari911
33919378907SAtari911        return $html;
34019378907SAtari911    }
34119378907SAtari911
342*9ccd446eSAtari911    private function renderEventListContent($events, $calId, $namespace, $themeStyles = null) {
34319378907SAtari911        if (empty($events)) {
34419378907SAtari911            return '<p class="no-events-msg">No events this month</p>';
34519378907SAtari911        }
34619378907SAtari911
347*9ccd446eSAtari911        // Default theme styles if not provided
348*9ccd446eSAtari911        if ($themeStyles === null) {
349*9ccd446eSAtari911            $theme = $this->getSidebarTheme();
350*9ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
351*9ccd446eSAtari911        }
352*9ccd446eSAtari911
3531d05cddcSAtari911        // Check for time conflicts
3541d05cddcSAtari911        $events = $this->checkTimeConflicts($events);
3551d05cddcSAtari911
356e3a9f44cSAtari911        // Sort by date ascending (chronological order - oldest first)
35719378907SAtari911        ksort($events);
35819378907SAtari911
359e3a9f44cSAtari911        // Sort events within each day by time
360e3a9f44cSAtari911        foreach ($events as $dateKey => &$dayEvents) {
361e3a9f44cSAtari911            usort($dayEvents, function($a, $b) {
3621d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
3631d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
3641d05cddcSAtari911
3651d05cddcSAtari911                // All-day events (no time) go to the TOP
3661d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
3671d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
3681d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
3691d05cddcSAtari911
3701d05cddcSAtari911                // Both have times, sort chronologically
371e3a9f44cSAtari911                return strcmp($timeA, $timeB);
372e3a9f44cSAtari911            });
373e3a9f44cSAtari911        }
374e3a9f44cSAtari911        unset($dayEvents); // Break reference
375e3a9f44cSAtari911
376e3a9f44cSAtari911        // Get today's date for comparison
377e3a9f44cSAtari911        $today = date('Y-m-d');
378e3a9f44cSAtari911        $firstFutureEventId = null;
379e3a9f44cSAtari911
3801d05cddcSAtari911        // Helper function to check if event is past (with 15-minute grace period for timed events)
3811d05cddcSAtari911        $isEventPast = function($dateKey, $time) use ($today) {
3821d05cddcSAtari911            // If event is on a past date, it's definitely past
3831d05cddcSAtari911            if ($dateKey < $today) {
3841d05cddcSAtari911                return true;
3851d05cddcSAtari911            }
3861d05cddcSAtari911
3871d05cddcSAtari911            // If event is on a future date, it's definitely not past
3881d05cddcSAtari911            if ($dateKey > $today) {
3891d05cddcSAtari911                return false;
3901d05cddcSAtari911            }
3911d05cddcSAtari911
3921d05cddcSAtari911            // Event is today - check time with grace period
3931d05cddcSAtari911            if ($time && $time !== '') {
3941d05cddcSAtari911                try {
3951d05cddcSAtari911                    $currentDateTime = new DateTime();
3961d05cddcSAtari911                    $eventDateTime = new DateTime($dateKey . ' ' . $time);
3971d05cddcSAtari911
3981d05cddcSAtari911                    // Add 15-minute grace period
3991d05cddcSAtari911                    $eventDateTime->modify('+15 minutes');
4001d05cddcSAtari911
4011d05cddcSAtari911                    // Event is past if current time > event time + 15 minutes
4021d05cddcSAtari911                    return $currentDateTime > $eventDateTime;
4031d05cddcSAtari911                } catch (Exception $e) {
4041d05cddcSAtari911                    // If time parsing fails, fall back to date-only comparison
4051d05cddcSAtari911                    return false;
4061d05cddcSAtari911                }
4071d05cddcSAtari911            }
4081d05cddcSAtari911
4091d05cddcSAtari911            // No time specified for today's event, treat as future
4101d05cddcSAtari911            return false;
4111d05cddcSAtari911        };
4121d05cddcSAtari911
4131d05cddcSAtari911        // Build HTML for each event - separate past/completed from future
4141d05cddcSAtari911        $pastHtml = '';
4151d05cddcSAtari911        $futureHtml = '';
4161d05cddcSAtari911        $pastCount = 0;
417e3a9f44cSAtari911
41819378907SAtari911        foreach ($events as $dateKey => $dayEvents) {
419e3a9f44cSAtari911
42019378907SAtari911            foreach ($dayEvents as $event) {
421e3a9f44cSAtari911                // Track first future/today event for auto-scroll
422e3a9f44cSAtari911                if (!$firstFutureEventId && $dateKey >= $today) {
423e3a9f44cSAtari911                    $firstFutureEventId = isset($event['id']) ? $event['id'] : '';
424e3a9f44cSAtari911                }
42519378907SAtari911                $eventId = isset($event['id']) ? $event['id'] : '';
42619378907SAtari911                $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
4271d05cddcSAtari911                $timeRaw = isset($event['time']) ? $event['time'] : '';
4281d05cddcSAtari911                $time = htmlspecialchars($timeRaw);
4291d05cddcSAtari911                $endTime = isset($event['endTime']) ? htmlspecialchars($event['endTime']) : '';
43019378907SAtari911                $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db';
43119378907SAtari911                $description = isset($event['description']) ? $event['description'] : '';
43219378907SAtari911                $isTask = isset($event['isTask']) ? $event['isTask'] : false;
43319378907SAtari911                $completed = isset($event['completed']) ? $event['completed'] : false;
43419378907SAtari911                $endDate = isset($event['endDate']) ? $event['endDate'] : '';
43519378907SAtari911
4361d05cddcSAtari911                // Use helper function to determine if event is past (with grace period)
4371d05cddcSAtari911                $isPast = $isEventPast($dateKey, $timeRaw);
4381d05cddcSAtari911                $isToday = $dateKey === $today;
4391d05cddcSAtari911
4401d05cddcSAtari911                // Check if event should be in past section
4411d05cddcSAtari911                // EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past
4421d05cddcSAtari911                $isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed;
4431d05cddcSAtari911                if ($isPastOrCompleted) {
4441d05cddcSAtari911                    $pastCount++;
4451d05cddcSAtari911                }
4461d05cddcSAtari911
4471d05cddcSAtari911                // Determine if task is past due (past date, is task, not completed)
4481d05cddcSAtari911                $isPastDue = $isPast && $isTask && !$completed;
4491d05cddcSAtari911
45019378907SAtari911                // Process description for wiki syntax, HTML, images, and links
451*9ccd446eSAtari911                $renderedDescription = $this->renderDescription($description, $themeStyles);
45219378907SAtari911
4531d05cddcSAtari911                // Convert to 12-hour format and handle time ranges
45419378907SAtari911                $displayTime = '';
45519378907SAtari911                if ($time) {
45619378907SAtari911                    $timeObj = DateTime::createFromFormat('H:i', $time);
45719378907SAtari911                    if ($timeObj) {
45819378907SAtari911                        $displayTime = $timeObj->format('g:i A');
4591d05cddcSAtari911
4601d05cddcSAtari911                        // Add end time if present and different from start time
4611d05cddcSAtari911                        if ($endTime && $endTime !== $time) {
4621d05cddcSAtari911                            $endTimeObj = DateTime::createFromFormat('H:i', $endTime);
4631d05cddcSAtari911                            if ($endTimeObj) {
4641d05cddcSAtari911                                $displayTime .= ' - ' . $endTimeObj->format('g:i A');
4651d05cddcSAtari911                            }
4661d05cddcSAtari911                        }
46719378907SAtari911                    } else {
46819378907SAtari911                        $displayTime = $time;
46919378907SAtari911                    }
47019378907SAtari911                }
47119378907SAtari911
47287ac9bf3SAtari911                // Format date display with day of week
473e3a9f44cSAtari911                // Use originalStartDate if this is a multi-month event continuation
474e3a9f44cSAtari911                $displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey;
475e3a9f44cSAtari911                $dateObj = new DateTime($displayDateKey);
47687ac9bf3SAtari911                $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24"
47719378907SAtari911
47819378907SAtari911                // Multi-day indicator
47919378907SAtari911                $multiDay = '';
480e3a9f44cSAtari911                if ($endDate && $endDate !== $displayDateKey) {
48119378907SAtari911                    $endObj = new DateTime($endDate);
48287ac9bf3SAtari911                    $multiDay = ' → ' . $endObj->format('D, M j');
48319378907SAtari911                }
48419378907SAtari911
48519378907SAtari911                $completedClass = $completed ? ' event-completed' : '';
4861d05cddcSAtari911                // Don't grey out past due tasks - they need attention!
4871d05cddcSAtari911                $pastClass = ($isPast && !$isPastDue) ? ' event-past' : '';
4881d05cddcSAtari911                $pastDueClass = $isPastDue ? ' event-pastdue' : '';
489e3a9f44cSAtari911                $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : '';
49019378907SAtari911
491*9ccd446eSAtari911                // For all themes: use CSS variables, only keep border-left-color as inline
492*9ccd446eSAtari911                $pastClickHandler = ($isPast && !$isPastDue) ? ' onclick="togglePastEventExpand(this)"' : '';
493*9ccd446eSAtari911                $eventHtml = '<div class="event-compact-item' . $completedClass . $pastClass . $pastDueClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ';"' . $pastClickHandler . $firstFutureAttr . '>';
4941d05cddcSAtari911                $eventHtml .= '<div class="event-info">';
495*9ccd446eSAtari911
4961d05cddcSAtari911                $eventHtml .= '<div class="event-title-row">';
4971d05cddcSAtari911                $eventHtml .= '<span class="event-title-compact">' . $title . '</span>';
4981d05cddcSAtari911                $eventHtml .= '</div>';
49919378907SAtari911
500e3a9f44cSAtari911                // For past events, hide meta and description (collapsed)
5011d05cddcSAtari911                // EXCEPTION: Past due tasks should show their details
5021d05cddcSAtari911                if (!$isPast || $isPastDue) {
5031d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact">';
5041d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
50519378907SAtari911                    if ($displayTime) {
5061d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
50719378907SAtari911                    }
5081d05cddcSAtari911                    // Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks
5091d05cddcSAtari911                    if ($isPastDue) {
5101d05cddcSAtari911                        $eventHtml .= ' <span class="event-pastdue-badge">PAST DUE</span>';
5111d05cddcSAtari911                    } elseif ($isToday) {
5121d05cddcSAtari911                        $eventHtml .= ' <span class="event-today-badge">TODAY</span>';
513e3a9f44cSAtari911                    }
5141d05cddcSAtari911                    // Add namespace badge - ALWAYS show if event has a namespace
515e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
516e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
517e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility
518e3a9f44cSAtari911                    }
5191d05cddcSAtari911                    // Show badge if namespace exists and is not empty
5201d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
5211d05cddcSAtari911                        $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>';
522e3a9f44cSAtari911                    }
5231d05cddcSAtari911
5241d05cddcSAtari911                    // Add conflict warning if event has time conflicts
5251d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
5261d05cddcSAtari911                        $conflictList = [];
5271d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
528*9ccd446eSAtari911                            $conflictText = $conflict['title'];
5291d05cddcSAtari911                            if (!empty($conflict['time'])) {
5301d05cddcSAtari911                                // Format time range
5311d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
5321d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
5331d05cddcSAtari911
5341d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
5351d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
5361d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
5371d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
5381d05cddcSAtari911                                } else {
5391d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
5401d05cddcSAtari911                                }
5411d05cddcSAtari911                            }
5421d05cddcSAtari911                            $conflictList[] = $conflictText;
5431d05cddcSAtari911                        }
5441d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
545*9ccd446eSAtari911                        $conflictJson = base64_encode(json_encode($conflictList));
5461d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
5471d05cddcSAtari911                    }
5481d05cddcSAtari911
5491d05cddcSAtari911                    $eventHtml .= '</span>';
5501d05cddcSAtari911                    $eventHtml .= '</div>';
55119378907SAtari911
55219378907SAtari911                    if ($description) {
5531d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>';
5541d05cddcSAtari911                    }
5551d05cddcSAtari911                } else {
5561d05cddcSAtari911                    // Past events: render with display:none for click-to-expand
5571d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact" style="display:none;">';
5581d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
5591d05cddcSAtari911                    if ($displayTime) {
5601d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
5611d05cddcSAtari911                    }
5621d05cddcSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
5631d05cddcSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
5641d05cddcSAtari911                        $eventNamespace = $event['_namespace'];
5651d05cddcSAtari911                    }
5661d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
5671d05cddcSAtari911                        $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>';
5681d05cddcSAtari911                    }
5691d05cddcSAtari911
5701d05cddcSAtari911                    // Add conflict warning if event has time conflicts
5711d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
5721d05cddcSAtari911                        $conflictList = [];
5731d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
574*9ccd446eSAtari911                            $conflictText = $conflict['title'];
5751d05cddcSAtari911                            if (!empty($conflict['time'])) {
5761d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
5771d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
5781d05cddcSAtari911
5791d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
5801d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
5811d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
5821d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
5831d05cddcSAtari911                                } else {
5841d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
5851d05cddcSAtari911                                }
5861d05cddcSAtari911                            }
5871d05cddcSAtari911                            $conflictList[] = $conflictText;
5881d05cddcSAtari911                        }
5891d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
590*9ccd446eSAtari911                        $conflictJson = base64_encode(json_encode($conflictList));
5911d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
5921d05cddcSAtari911                    }
5931d05cddcSAtari911
5941d05cddcSAtari911                    $eventHtml .= '</span>';
5951d05cddcSAtari911                    $eventHtml .= '</div>';
5961d05cddcSAtari911
5971d05cddcSAtari911                    if ($description) {
5981d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '</div>';
59919378907SAtari911                    }
600e3a9f44cSAtari911                }
60119378907SAtari911
6021d05cddcSAtari911                $eventHtml .= '</div>'; // event-info
60319378907SAtari911
604e3a9f44cSAtari911                // Use stored namespace from event, fallback to passed namespace
605e3a9f44cSAtari911                $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace;
606e3a9f44cSAtari911
6071d05cddcSAtari911                $eventHtml .= '<div class="event-actions-compact">';
6081d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">��️</button>';
6091d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>';
6101d05cddcSAtari911                $eventHtml .= '</div>';
61119378907SAtari911
61219378907SAtari911                // Checkbox for tasks - ON THE FAR RIGHT
61319378907SAtari911                if ($isTask) {
61419378907SAtari911                    $checked = $completed ? 'checked' : '';
6151d05cddcSAtari911                    $eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">';
61619378907SAtari911                }
61719378907SAtari911
6181d05cddcSAtari911                $eventHtml .= '</div>';
6191d05cddcSAtari911
6201d05cddcSAtari911                // Add to appropriate section
6211d05cddcSAtari911                if ($isPastOrCompleted) {
6221d05cddcSAtari911                    $pastHtml .= $eventHtml;
6231d05cddcSAtari911                } else {
6241d05cddcSAtari911                    $futureHtml .= $eventHtml;
6251d05cddcSAtari911                }
6261d05cddcSAtari911            }
6271d05cddcSAtari911        }
6281d05cddcSAtari911
6291d05cddcSAtari911        // Build final HTML with collapsible past events section
6301d05cddcSAtari911        $html = '';
6311d05cddcSAtari911
6321d05cddcSAtari911        // Add collapsible past events section if any exist
6331d05cddcSAtari911        if ($pastCount > 0) {
6341d05cddcSAtari911            $html .= '<div class="past-events-section">';
6351d05cddcSAtari911            $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">';
6361d05cddcSAtari911            $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> ';
6371d05cddcSAtari911            $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>';
63819378907SAtari911            $html .= '</div>';
6391d05cddcSAtari911            $html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">';
6401d05cddcSAtari911            $html .= $pastHtml;
6411d05cddcSAtari911            $html .= '</div>';
6421d05cddcSAtari911            $html .= '</div>';
6431d05cddcSAtari911        }
644e3a9f44cSAtari911
6451d05cddcSAtari911        // Add future events
6461d05cddcSAtari911        $html .= $futureHtml;
64719378907SAtari911
64819378907SAtari911        return $html;
64919378907SAtari911    }
65019378907SAtari911
6511d05cddcSAtari911    /**
6521d05cddcSAtari911     * Check for time conflicts between events
6531d05cddcSAtari911     */
6541d05cddcSAtari911    private function checkTimeConflicts($events) {
6551d05cddcSAtari911        // Group events by date
6561d05cddcSAtari911        $eventsByDate = [];
6571d05cddcSAtari911        foreach ($events as $date => $dateEvents) {
6581d05cddcSAtari911            if (!is_array($dateEvents)) continue;
6591d05cddcSAtari911
6601d05cddcSAtari911            foreach ($dateEvents as $evt) {
6611d05cddcSAtari911                if (empty($evt['time'])) continue; // Skip all-day events
6621d05cddcSAtari911
6631d05cddcSAtari911                if (!isset($eventsByDate[$date])) {
6641d05cddcSAtari911                    $eventsByDate[$date] = [];
6651d05cddcSAtari911                }
6661d05cddcSAtari911                $eventsByDate[$date][] = $evt;
6671d05cddcSAtari911            }
6681d05cddcSAtari911        }
6691d05cddcSAtari911
6701d05cddcSAtari911        // Check for overlaps on each date
6711d05cddcSAtari911        foreach ($eventsByDate as $date => $dateEvents) {
6721d05cddcSAtari911            for ($i = 0; $i < count($dateEvents); $i++) {
6731d05cddcSAtari911                for ($j = $i + 1; $j < count($dateEvents); $j++) {
6741d05cddcSAtari911                    if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) {
6751d05cddcSAtari911                        // Mark both events as conflicting
6761d05cddcSAtari911                        $dateEvents[$i]['hasConflict'] = true;
6771d05cddcSAtari911                        $dateEvents[$j]['hasConflict'] = true;
6781d05cddcSAtari911
6791d05cddcSAtari911                        // Store conflict info
6801d05cddcSAtari911                        if (!isset($dateEvents[$i]['conflictsWith'])) {
6811d05cddcSAtari911                            $dateEvents[$i]['conflictsWith'] = [];
6821d05cddcSAtari911                        }
6831d05cddcSAtari911                        if (!isset($dateEvents[$j]['conflictsWith'])) {
6841d05cddcSAtari911                            $dateEvents[$j]['conflictsWith'] = [];
6851d05cddcSAtari911                        }
6861d05cddcSAtari911
6871d05cddcSAtari911                        $dateEvents[$i]['conflictsWith'][] = [
6881d05cddcSAtari911                            'id' => $dateEvents[$j]['id'],
6891d05cddcSAtari911                            'title' => $dateEvents[$j]['title'],
6901d05cddcSAtari911                            'time' => $dateEvents[$j]['time'],
6911d05cddcSAtari911                            'endTime' => isset($dateEvents[$j]['endTime']) ? $dateEvents[$j]['endTime'] : ''
6921d05cddcSAtari911                        ];
6931d05cddcSAtari911
6941d05cddcSAtari911                        $dateEvents[$j]['conflictsWith'][] = [
6951d05cddcSAtari911                            'id' => $dateEvents[$i]['id'],
6961d05cddcSAtari911                            'title' => $dateEvents[$i]['title'],
6971d05cddcSAtari911                            'time' => $dateEvents[$i]['time'],
6981d05cddcSAtari911                            'endTime' => isset($dateEvents[$i]['endTime']) ? $dateEvents[$i]['endTime'] : ''
6991d05cddcSAtari911                        ];
7001d05cddcSAtari911                    }
7011d05cddcSAtari911                }
7021d05cddcSAtari911            }
7031d05cddcSAtari911
7041d05cddcSAtari911            // Update the events array with conflict information
7051d05cddcSAtari911            foreach ($events[$date] as &$evt) {
7061d05cddcSAtari911                foreach ($dateEvents as $checkedEvt) {
7071d05cddcSAtari911                    if ($evt['id'] === $checkedEvt['id']) {
7081d05cddcSAtari911                        if (isset($checkedEvt['hasConflict'])) {
7091d05cddcSAtari911                            $evt['hasConflict'] = $checkedEvt['hasConflict'];
7101d05cddcSAtari911                        }
7111d05cddcSAtari911                        if (isset($checkedEvt['conflictsWith'])) {
7121d05cddcSAtari911                            $evt['conflictsWith'] = $checkedEvt['conflictsWith'];
7131d05cddcSAtari911                        }
7141d05cddcSAtari911                        break;
7151d05cddcSAtari911                    }
7161d05cddcSAtari911                }
7171d05cddcSAtari911            }
7181d05cddcSAtari911        }
7191d05cddcSAtari911
7201d05cddcSAtari911        return $events;
7211d05cddcSAtari911    }
7221d05cddcSAtari911
7231d05cddcSAtari911    /**
7241d05cddcSAtari911     * Check if two events overlap in time
7251d05cddcSAtari911     */
7261d05cddcSAtari911    private function eventsOverlap($evt1, $evt2) {
7271d05cddcSAtari911        if (empty($evt1['time']) || empty($evt2['time'])) {
7281d05cddcSAtari911            return false; // All-day events don't conflict
7291d05cddcSAtari911        }
7301d05cddcSAtari911
7311d05cddcSAtari911        $start1 = $evt1['time'];
7321d05cddcSAtari911        $end1 = isset($evt1['endTime']) && !empty($evt1['endTime']) ? $evt1['endTime'] : $evt1['time'];
7331d05cddcSAtari911
7341d05cddcSAtari911        $start2 = $evt2['time'];
7351d05cddcSAtari911        $end2 = isset($evt2['endTime']) && !empty($evt2['endTime']) ? $evt2['endTime'] : $evt2['time'];
7361d05cddcSAtari911
7371d05cddcSAtari911        // Convert to minutes for easier comparison
7381d05cddcSAtari911        $start1Mins = $this->timeToMinutes($start1);
7391d05cddcSAtari911        $end1Mins = $this->timeToMinutes($end1);
7401d05cddcSAtari911        $start2Mins = $this->timeToMinutes($start2);
7411d05cddcSAtari911        $end2Mins = $this->timeToMinutes($end2);
7421d05cddcSAtari911
7431d05cddcSAtari911        // Check for overlap: start1 < end2 AND start2 < end1
7441d05cddcSAtari911        return $start1Mins < $end2Mins && $start2Mins < $end1Mins;
7451d05cddcSAtari911    }
7461d05cddcSAtari911
7471d05cddcSAtari911    /**
7481d05cddcSAtari911     * Convert HH:MM time to minutes since midnight
7491d05cddcSAtari911     */
7501d05cddcSAtari911    private function timeToMinutes($timeStr) {
7511d05cddcSAtari911        $parts = explode(':', $timeStr);
7521d05cddcSAtari911        if (count($parts) !== 2) return 0;
7531d05cddcSAtari911
7541d05cddcSAtari911        return (int)$parts[0] * 60 + (int)$parts[1];
7551d05cddcSAtari911    }
7561d05cddcSAtari911
75719378907SAtari911    private function renderEventPanelOnly($data) {
75819378907SAtari911        $year = (int)$data['year'];
75919378907SAtari911        $month = (int)$data['month'];
76019378907SAtari911        $namespace = $data['namespace'];
76187ac9bf3SAtari911        $height = isset($data['height']) ? $data['height'] : '400px';
76287ac9bf3SAtari911
76387ac9bf3SAtari911        // Validate height format (must be px, em, rem, vh, or %)
76487ac9bf3SAtari911        if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) {
76587ac9bf3SAtari911            $height = '400px'; // Default fallback
76687ac9bf3SAtari911        }
76719378907SAtari911
768*9ccd446eSAtari911        // Get theme
769*9ccd446eSAtari911        $theme = $this->getSidebarTheme();
770*9ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
771*9ccd446eSAtari911
772e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
773e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
774e3a9f44cSAtari911
775e3a9f44cSAtari911        if ($isMultiNamespace) {
776e3a9f44cSAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
777e3a9f44cSAtari911        } else {
77819378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
779e3a9f44cSAtari911        }
78019378907SAtari911        $calId = 'panel_' . md5(serialize($data) . microtime());
78119378907SAtari911
78219378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
78319378907SAtari911
78419378907SAtari911        $prevMonth = $month - 1;
78519378907SAtari911        $prevYear = $year;
78619378907SAtari911        if ($prevMonth < 1) {
78719378907SAtari911            $prevMonth = 12;
78819378907SAtari911            $prevYear--;
78919378907SAtari911        }
79019378907SAtari911
79119378907SAtari911        $nextMonth = $month + 1;
79219378907SAtari911        $nextYear = $year;
79319378907SAtari911        if ($nextMonth > 12) {
79419378907SAtari911            $nextMonth = 1;
79519378907SAtari911            $nextYear++;
79619378907SAtari911        }
79719378907SAtari911
798*9ccd446eSAtari911        // Determine button text color based on theme
799*9ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
800*9ccd446eSAtari911
801*9ccd446eSAtari911        $html = '<div class="event-panel-standalone" id="' . $calId . '" data-height="' . htmlspecialchars($height) . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '" data-theme="' . $theme . '" data-theme-styles="' . htmlspecialchars(json_encode($themeStyles)) . '">';
802*9ccd446eSAtari911
803*9ccd446eSAtari911        // Inject CSS variables for this panel instance - same as main calendar
804*9ccd446eSAtari911        $html .= '<style>
805*9ccd446eSAtari911        #' . $calId . ' {
806*9ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
807*9ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
808*9ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
809*9ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
810*9ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
811*9ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
812*9ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
813*9ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
814*9ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
815*9ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
816*9ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
817*9ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
818*9ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
819*9ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
820*9ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
821*9ccd446eSAtari911        }
822*9ccd446eSAtari911        #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
823*9ccd446eSAtari911        </style>';
82419378907SAtari911
8251d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
8261d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
8271d05cddcSAtari911
8281d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
8291d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
8301d05cddcSAtari911
8311d05cddcSAtari911        // Compact two-row header designed for ~500px width
8321d05cddcSAtari911        $html .= '<div class="panel-header-compact">';
8331d05cddcSAtari911
8341d05cddcSAtari911        // Row 1: Navigation and title
8351d05cddcSAtari911        $html .= '<div class="panel-header-row-1">';
8361d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
8371d05cddcSAtari911
8381d05cddcSAtari911        // Compact month name (e.g. "Feb 2026" instead of "February 2026 Events")
8391d05cddcSAtari911        $shortMonthName = date('M Y', mktime(0, 0, 0, $month, 1, $year));
8401d05cddcSAtari911        $html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $shortMonthName . '</h3>';
8411d05cddcSAtari911
8421d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
8431d05cddcSAtari911
8441d05cddcSAtari911        // Namespace badge (if applicable)
84587ac9bf3SAtari911        if ($namespace) {
846e3a9f44cSAtari911            if ($isMultiNamespace) {
847e3a9f44cSAtari911                if (strpos($namespace, '*') !== false) {
8481d05cddcSAtari911                    $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>';
849e3a9f44cSAtari911                } else {
850e3a9f44cSAtari911                    $namespaceList = array_map('trim', explode(';', $namespace));
8511d05cddcSAtari911                    $nsCount = count($namespaceList);
8521d05cddcSAtari911                    $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars(implode(', ', $namespaceList)) . '">' . $nsCount . ' NS</span>';
853e3a9f44cSAtari911                }
854e3a9f44cSAtari911            } else {
8551d05cddcSAtari911                $isFiltering = ($namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false);
8561d05cddcSAtari911                if ($isFiltering) {
8571d05cddcSAtari911                    $html .= '<span class="panel-ns-badge filter-on" title="Filtering by ' . htmlspecialchars($namespace) . ' - click to clear" onclick="clearNamespaceFilterPanel(\'' . $calId . '\')">' . htmlspecialchars($namespace) . ' ✕</span>';
8581d05cddcSAtari911                } else {
8591d05cddcSAtari911                    $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>';
86087ac9bf3SAtari911                }
861e3a9f44cSAtari911            }
8621d05cddcSAtari911        }
8631d05cddcSAtari911
8641d05cddcSAtari911        $html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
86519378907SAtari911        $html .= '</div>';
86619378907SAtari911
8671d05cddcSAtari911        // Row 2: Search and add button
8681d05cddcSAtari911        $html .= '<div class="panel-header-row-2">';
8691d05cddcSAtari911        $html .= '<div class="panel-search-box">';
8701d05cddcSAtari911        $html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placeholder="Search events..." oninput="filterEvents(\'' . $calId . '\', this.value)">';
8711d05cddcSAtari911        $html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
8721d05cddcSAtari911        $html .= '</div>';
8731d05cddcSAtari911        $html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
8741d05cddcSAtari911        $html .= '</div>';
8751d05cddcSAtari911
87619378907SAtari911        $html .= '</div>';
87719378907SAtari911
87887ac9bf3SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">';
87919378907SAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace);
88019378907SAtari911        $html .= '</div>';
88119378907SAtari911
88219378907SAtari911        $html .= $this->renderEventDialog($calId, $namespace);
88319378907SAtari911
88487ac9bf3SAtari911        // Month/Year picker for event panel
885*9ccd446eSAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
88687ac9bf3SAtari911
88719378907SAtari911        $html .= '</div>';
88819378907SAtari911
88919378907SAtari911        return $html;
89019378907SAtari911    }
89119378907SAtari911
89219378907SAtari911    private function renderStandaloneEventList($data) {
89319378907SAtari911        $namespace = $data['namespace'];
8941d05cddcSAtari911        // If no namespace specified, show all namespaces
8951d05cddcSAtari911        if (empty($namespace)) {
8961d05cddcSAtari911            $namespace = '*';
8971d05cddcSAtari911        }
89819378907SAtari911        $daterange = $data['daterange'];
89919378907SAtari911        $date = $data['date'];
900e3a9f44cSAtari911        $range = isset($data['range']) ? strtolower($data['range']) : '';
90187ac9bf3SAtari911        $today = isset($data['today']) ? true : false;
902e3a9f44cSAtari911        $sidebar = isset($data['sidebar']) ? true : false;
9031d05cddcSAtari911        $showchecked = isset($data['showchecked']) ? true : false; // New parameter
9041d05cddcSAtari911        $noheader = isset($data['noheader']) ? true : false; // New parameter to hide header
90519378907SAtari911
906e3a9f44cSAtari911        // Handle "range" parameter - day, week, or month
907e3a9f44cSAtari911        if ($range === 'day') {
9081d05cddcSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
90987ac9bf3SAtari911            $endDate = date('Y-m-d');
910e3a9f44cSAtari911            $headerText = 'Today';
911e3a9f44cSAtari911        } elseif ($range === 'week') {
9121d05cddcSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
9131d05cddcSAtari911            $endDateTime = new DateTime();
914e3a9f44cSAtari911            $endDateTime->modify('+7 days');
915e3a9f44cSAtari911            $endDate = $endDateTime->format('Y-m-d');
916e3a9f44cSAtari911            $headerText = 'This Week';
917e3a9f44cSAtari911        } elseif ($range === 'month') {
9181d05cddcSAtari911            $startDate = date('Y-m-01', strtotime('-1 month')); // Include previous month for past due tasks
919e3a9f44cSAtari911            $endDate = date('Y-m-t'); // Last of current month
9201d05cddcSAtari911            $dt = new DateTime();
921e3a9f44cSAtari911            $headerText = $dt->format('F Y');
922e3a9f44cSAtari911        } elseif ($sidebar) {
9231d05cddcSAtari911            // NEW: Sidebar widget - load current week's events
924*9ccd446eSAtari911            $weekStartDay = $this->getWeekStartDay(); // Get saved preference
925*9ccd446eSAtari911
926*9ccd446eSAtari911            if ($weekStartDay === 'monday') {
927*9ccd446eSAtari911                // Monday start
9281d05cddcSAtari911                $weekStart = date('Y-m-d', strtotime('monday this week'));
9291d05cddcSAtari911                $weekEnd = date('Y-m-d', strtotime('sunday this week'));
930*9ccd446eSAtari911            } else {
931*9ccd446eSAtari911                // Sunday start (default - US/Canada standard)
932*9ccd446eSAtari911                $today = date('w'); // 0 (Sun) to 6 (Sat)
933*9ccd446eSAtari911                if ($today == 0) {
934*9ccd446eSAtari911                    // Today is Sunday
935*9ccd446eSAtari911                    $weekStart = date('Y-m-d');
936*9ccd446eSAtari911                } else {
937*9ccd446eSAtari911                    // Monday-Saturday: go back to last Sunday
938*9ccd446eSAtari911                    $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
939*9ccd446eSAtari911                }
940*9ccd446eSAtari911                $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
941*9ccd446eSAtari911            }
9421d05cddcSAtari911
943*9ccd446eSAtari911            // Load events for the entire week PLUS tomorrow (if tomorrow is outside week)
944*9ccd446eSAtari911            // PLUS next 2 weeks for Important events
9451d05cddcSAtari911            $start = new DateTime($weekStart);
9461d05cddcSAtari911            $end = new DateTime($weekEnd);
947*9ccd446eSAtari911
948*9ccd446eSAtari911            // Check if we need to extend to include tomorrow
949*9ccd446eSAtari911            $tomorrowDate = date('Y-m-d', strtotime('+1 day'));
950*9ccd446eSAtari911            if ($tomorrowDate > $weekEnd) {
951*9ccd446eSAtari911                // Tomorrow is outside the week, extend end date to include it
952*9ccd446eSAtari911                $end = new DateTime($tomorrowDate);
953*9ccd446eSAtari911            }
954*9ccd446eSAtari911
955*9ccd446eSAtari911            // Extend 2 weeks into the future for Important events
956*9ccd446eSAtari911            $twoWeeksOut = date('Y-m-d', strtotime($weekEnd . ' +14 days'));
957*9ccd446eSAtari911            $end = new DateTime($twoWeeksOut);
958*9ccd446eSAtari911
9591d05cddcSAtari911            $end->modify('+1 day'); // DatePeriod excludes end date
9601d05cddcSAtari911            $interval = new DateInterval('P1D');
9611d05cddcSAtari911            $period = new DatePeriod($start, $interval, $end);
9621d05cddcSAtari911
9631d05cddcSAtari911            $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
9641d05cddcSAtari911            $allEvents = [];
9651d05cddcSAtari911            $loadedMonths = [];
9661d05cddcSAtari911
9671d05cddcSAtari911            foreach ($period as $dt) {
9681d05cddcSAtari911                $year = (int)$dt->format('Y');
9691d05cddcSAtari911                $month = (int)$dt->format('n');
9701d05cddcSAtari911                $dateKey = $dt->format('Y-m-d');
9711d05cddcSAtari911
9721d05cddcSAtari911                $monthKey = $year . '-' . $month . '-' . $namespace;
9731d05cddcSAtari911
9741d05cddcSAtari911                if (!isset($loadedMonths[$monthKey])) {
9751d05cddcSAtari911                    if ($isMultiNamespace) {
9761d05cddcSAtari911                        $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
9771d05cddcSAtari911                    } else {
9781d05cddcSAtari911                        $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
9791d05cddcSAtari911                    }
9801d05cddcSAtari911                }
9811d05cddcSAtari911
9821d05cddcSAtari911                $monthEvents = $loadedMonths[$monthKey];
9831d05cddcSAtari911
9841d05cddcSAtari911                if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
9851d05cddcSAtari911                    $allEvents[$dateKey] = $monthEvents[$dateKey];
9861d05cddcSAtari911                }
9871d05cddcSAtari911            }
9881d05cddcSAtari911
9891d05cddcSAtari911            // Apply time conflict detection
9901d05cddcSAtari911            $allEvents = $this->checkTimeConflicts($allEvents);
9911d05cddcSAtari911
9921d05cddcSAtari911            $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8);
9931d05cddcSAtari911
9941d05cddcSAtari911            // Render sidebar widget and return immediately
9951d05cddcSAtari911            return $this->renderSidebarWidget($allEvents, $namespace, $calId);
996e3a9f44cSAtari911        } elseif ($today) {
997e3a9f44cSAtari911            $startDate = date('Y-m-d');
998e3a9f44cSAtari911            $endDate = date('Y-m-d');
999e3a9f44cSAtari911            $headerText = 'Today';
100087ac9bf3SAtari911        } elseif ($daterange) {
100119378907SAtari911            list($startDate, $endDate) = explode(':', $daterange);
1002e3a9f44cSAtari911            $start = new DateTime($startDate);
1003e3a9f44cSAtari911            $end = new DateTime($endDate);
1004e3a9f44cSAtari911            $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y');
100519378907SAtari911        } elseif ($date) {
100619378907SAtari911            $startDate = $date;
100719378907SAtari911            $endDate = $date;
1008e3a9f44cSAtari911            $dt = new DateTime($date);
1009e3a9f44cSAtari911            $headerText = $dt->format('l, F j, Y');
101019378907SAtari911        } else {
101119378907SAtari911            $startDate = date('Y-m-01');
101219378907SAtari911            $endDate = date('Y-m-t');
1013e3a9f44cSAtari911            $dt = new DateTime($startDate);
1014e3a9f44cSAtari911            $headerText = $dt->format('F Y');
101519378907SAtari911        }
101619378907SAtari911
1017e3a9f44cSAtari911        // Load all events in date range
101819378907SAtari911        $allEvents = array();
101919378907SAtari911        $start = new DateTime($startDate);
102019378907SAtari911        $end = new DateTime($endDate);
102119378907SAtari911        $end->modify('+1 day');
102219378907SAtari911
102319378907SAtari911        $interval = new DateInterval('P1D');
102419378907SAtari911        $period = new DatePeriod($start, $interval, $end);
102519378907SAtari911
1026e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
1027e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
1028e3a9f44cSAtari911
102919378907SAtari911        static $loadedMonths = array();
103019378907SAtari911
103119378907SAtari911        foreach ($period as $dt) {
103219378907SAtari911            $year = (int)$dt->format('Y');
103319378907SAtari911            $month = (int)$dt->format('n');
103419378907SAtari911            $dateKey = $dt->format('Y-m-d');
103519378907SAtari911
1036e3a9f44cSAtari911            $monthKey = $year . '-' . $month . '-' . $namespace;
103719378907SAtari911
103819378907SAtari911            if (!isset($loadedMonths[$monthKey])) {
1039e3a9f44cSAtari911                if ($isMultiNamespace) {
1040e3a9f44cSAtari911                    $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
1041e3a9f44cSAtari911                } else {
104219378907SAtari911                    $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
104319378907SAtari911                }
1044e3a9f44cSAtari911            }
104519378907SAtari911
104619378907SAtari911            $monthEvents = $loadedMonths[$monthKey];
104719378907SAtari911
104819378907SAtari911            if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
104919378907SAtari911                $allEvents[$dateKey] = $monthEvents[$dateKey];
105019378907SAtari911            }
105119378907SAtari911        }
105219378907SAtari911
10531d05cddcSAtari911        // Sort events by date (already sorted by dateKey), then by time within each day
10541d05cddcSAtari911        foreach ($allEvents as $dateKey => &$dayEvents) {
10551d05cddcSAtari911            usort($dayEvents, function($a, $b) {
10561d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
10571d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
10581d05cddcSAtari911
10591d05cddcSAtari911                // All-day events (no time) go to the TOP
10601d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
10611d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
10621d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
10631d05cddcSAtari911
10641d05cddcSAtari911                // Both have times, sort chronologically
10651d05cddcSAtari911                return strcmp($timeA, $timeB);
10661d05cddcSAtari911            });
10671d05cddcSAtari911        }
10681d05cddcSAtari911        unset($dayEvents); // Break reference
10691d05cddcSAtari911
1070e3a9f44cSAtari911        // Simple 2-line display widget
10711d05cddcSAtari911        $calId = 'eventlist_' . uniqid();
10721d05cddcSAtari911        $html = '<div class="eventlist-simple" id="' . $calId . '">';
10731d05cddcSAtari911
10741d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
10751d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
10761d05cddcSAtari911
10771d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
10781d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
10791d05cddcSAtari911
10801d05cddcSAtari911        // Add compact header with date and clock for "today" mode (unless noheader is set)
10811d05cddcSAtari911        if ($today && !empty($allEvents) && !$noheader) {
10821d05cddcSAtari911            $todayDate = new DateTime();
10831d05cddcSAtari911            $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026"
10841d05cddcSAtari911            $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM"
10851d05cddcSAtari911
10861d05cddcSAtari911            $html .= '<div class="eventlist-today-header">';
10871d05cddcSAtari911            $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>';
10881d05cddcSAtari911            $html .= '<div class="eventlist-bottom-info">';
10891d05cddcSAtari911            $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '">--°</span></span>';
10901d05cddcSAtari911            $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>';
10911d05cddcSAtari911            $html .= '</div>';
10921d05cddcSAtari911
10931d05cddcSAtari911            // Three CPU/Memory bars (all update live)
10941d05cddcSAtari911            $html .= '<div class="eventlist-stats-container">';
10951d05cddcSAtari911
10961d05cddcSAtari911            // 5-minute load average (green, updates every 2 seconds)
10971d05cddcSAtari911            $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $calId . '(\'green\')" onmouseout="hideTooltip_' . $calId . '(\'green\')">';
10981d05cddcSAtari911            $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>';
10991d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
11001d05cddcSAtari911            $html .= '</div>';
11011d05cddcSAtari911
11021d05cddcSAtari911            // Real-time CPU (purple, updates with 5-sec average)
11031d05cddcSAtari911            $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $calId . '(\'purple\')" onmouseout="hideTooltip_' . $calId . '(\'purple\')">';
11041d05cddcSAtari911            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>';
11051d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
11061d05cddcSAtari911            $html .= '</div>';
11071d05cddcSAtari911
11081d05cddcSAtari911            // Real-time Memory (orange, updates)
11091d05cddcSAtari911            $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $calId . '(\'orange\')" onmouseout="hideTooltip_' . $calId . '(\'orange\')">';
11101d05cddcSAtari911            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>';
11111d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
11121d05cddcSAtari911            $html .= '</div>';
11131d05cddcSAtari911
11141d05cddcSAtari911            $html .= '</div>';
11151d05cddcSAtari911            $html .= '</div>';
11161d05cddcSAtari911
11171d05cddcSAtari911            // Add JavaScript to update clock and weather
11181d05cddcSAtari911            $html .= '<script>
11191d05cddcSAtari911(function() {
11201d05cddcSAtari911    // Update clock every second
11211d05cddcSAtari911    function updateClock() {
11221d05cddcSAtari911        const now = new Date();
11231d05cddcSAtari911        let hours = now.getHours();
11241d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
11251d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
11261d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
11271d05cddcSAtari911        hours = hours % 12 || 12;
11281d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
11291d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
11301d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
11311d05cddcSAtari911    }
11321d05cddcSAtari911    setInterval(updateClock, 1000);
11331d05cddcSAtari911
11341d05cddcSAtari911    // Fetch weather (geolocation-based)
11351d05cddcSAtari911    function updateWeather() {
11361d05cddcSAtari911        if ("geolocation" in navigator) {
11371d05cddcSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
11381d05cddcSAtari911                const lat = position.coords.latitude;
11391d05cddcSAtari911                const lon = position.coords.longitude;
11401d05cddcSAtari911
11411d05cddcSAtari911                // Use Open-Meteo API (free, no key required)
11421d05cddcSAtari911                fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true&temperature_unit=fahrenheit`)
11431d05cddcSAtari911                    .then(response => response.json())
11441d05cddcSAtari911                    .then(data => {
11451d05cddcSAtari911                        if (data.current_weather) {
11461d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
11471d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
11481d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
11491d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
11501d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
11511d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
11521d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
11531d05cddcSAtari911                        }
11541d05cddcSAtari911                    })
11551d05cddcSAtari911                    .catch(error => {
11561d05cddcSAtari911                        console.log("Weather fetch error:", error);
11571d05cddcSAtari911                    });
11581d05cddcSAtari911            }, function(error) {
11591d05cddcSAtari911                // If geolocation fails, use Sacramento as default
11601d05cddcSAtari911                fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944&current_weather=true&temperature_unit=fahrenheit")
11611d05cddcSAtari911                    .then(response => response.json())
11621d05cddcSAtari911                    .then(data => {
11631d05cddcSAtari911                        if (data.current_weather) {
11641d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
11651d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
11661d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
11671d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
11681d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
11691d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
11701d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
11711d05cddcSAtari911                        }
11721d05cddcSAtari911                    })
11731d05cddcSAtari911                    .catch(err => console.log("Weather error:", err));
11741d05cddcSAtari911            });
11751d05cddcSAtari911        } else {
11761d05cddcSAtari911            // No geolocation, use Sacramento
11771d05cddcSAtari911            fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944&current_weather=true&temperature_unit=fahrenheit")
11781d05cddcSAtari911                .then(response => response.json())
11791d05cddcSAtari911                .then(data => {
11801d05cddcSAtari911                    if (data.current_weather) {
11811d05cddcSAtari911                        const temp = Math.round(data.current_weather.temperature);
11821d05cddcSAtari911                        const weatherCode = data.current_weather.weathercode;
11831d05cddcSAtari911                        const icon = getWeatherIcon(weatherCode);
11841d05cddcSAtari911                        const iconEl = document.getElementById("weather-icon-' . $calId . '");
11851d05cddcSAtari911                        const tempEl = document.getElementById("weather-temp-' . $calId . '");
11861d05cddcSAtari911                        if (iconEl) iconEl.textContent = icon;
11871d05cddcSAtari911                        if (tempEl) tempEl.innerHTML = temp + "&deg;";
11881d05cddcSAtari911                    }
11891d05cddcSAtari911                })
11901d05cddcSAtari911                .catch(err => console.log("Weather error:", err));
11911d05cddcSAtari911        }
11921d05cddcSAtari911    }
11931d05cddcSAtari911
11941d05cddcSAtari911    // WMO Weather interpretation codes
11951d05cddcSAtari911    function getWeatherIcon(code) {
11961d05cddcSAtari911        const icons = {
11971d05cddcSAtari911            0: "☀️",   // Clear sky
11981d05cddcSAtari911            1: "��️",   // Mainly clear
11991d05cddcSAtari911            2: "⛅",   // Partly cloudy
12001d05cddcSAtari911            3: "☁️",   // Overcast
12011d05cddcSAtari911            45: "��️",  // Fog
12021d05cddcSAtari911            48: "��️",  // Depositing rime fog
12031d05cddcSAtari911            51: "��️",  // Light drizzle
12041d05cddcSAtari911            53: "��️",  // Moderate drizzle
12051d05cddcSAtari911            55: "��️",  // Dense drizzle
12061d05cddcSAtari911            61: "��️",  // Slight rain
12071d05cddcSAtari911            63: "��️",  // Moderate rain
12081d05cddcSAtari911            65: "⛈️",  // Heavy rain
12091d05cddcSAtari911            71: "��️",  // Slight snow
12101d05cddcSAtari911            73: "��️",  // Moderate snow
12111d05cddcSAtari911            75: "❄️",  // Heavy snow
12121d05cddcSAtari911            77: "��️",  // Snow grains
12131d05cddcSAtari911            80: "��️",  // Slight rain showers
12141d05cddcSAtari911            81: "��️",  // Moderate rain showers
12151d05cddcSAtari911            82: "⛈️",  // Violent rain showers
12161d05cddcSAtari911            85: "��️",  // Slight snow showers
12171d05cddcSAtari911            86: "❄️",  // Heavy snow showers
12181d05cddcSAtari911            95: "⛈️",  // Thunderstorm
12191d05cddcSAtari911            96: "⛈️",  // Thunderstorm with slight hail
12201d05cddcSAtari911            99: "⛈️"   // Thunderstorm with heavy hail
12211d05cddcSAtari911        };
12221d05cddcSAtari911        return icons[code] || "��️";
12231d05cddcSAtari911    }
12241d05cddcSAtari911
12251d05cddcSAtari911    // Update weather immediately and every 10 minutes
12261d05cddcSAtari911    updateWeather();
12271d05cddcSAtari911    setInterval(updateWeather, 600000);
12281d05cddcSAtari911
12291d05cddcSAtari911    // CPU load history for 4-second rolling average
12301d05cddcSAtari911    const cpuHistory = [];
12311d05cddcSAtari911    const CPU_HISTORY_SIZE = 2; // 2 samples × 2 seconds = 4 seconds
12321d05cddcSAtari911
12331d05cddcSAtari911    // Store latest system stats for tooltips
12341d05cddcSAtari911    let latestStats = {
12351d05cddcSAtari911        load: {"1min": 0, "5min": 0, "15min": 0},
12361d05cddcSAtari911        uptime: "",
12371d05cddcSAtari911        memory_details: {},
12381d05cddcSAtari911        top_processes: []
12391d05cddcSAtari911    };
12401d05cddcSAtari911
12411d05cddcSAtari911    // Tooltip functions
12421d05cddcSAtari911    window["showTooltip_' . $calId . '"] = function(color) {
12431d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
12441d05cddcSAtari911        if (!tooltip) {
12451d05cddcSAtari911            console.log("Tooltip element not found for color:", color);
12461d05cddcSAtari911            return;
12471d05cddcSAtari911        }
12481d05cddcSAtari911
12491d05cddcSAtari911        console.log("Showing tooltip for:", color, "latestStats:", latestStats);
12501d05cddcSAtari911
12511d05cddcSAtari911        let content = "";
12521d05cddcSAtari911
12531d05cddcSAtari911        if (color === "green") {
12541d05cddcSAtari911            // Green bar: Load averages and uptime
12551d05cddcSAtari911            content = "<div class=\"tooltip-title\">CPU Load Average</div>";
12561d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
12571d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
12581d05cddcSAtari911            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
12591d05cddcSAtari911            if (latestStats.uptime) {
12601d05cddcSAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\">Uptime: " + latestStats.uptime + "</div>";
12611d05cddcSAtari911            }
12621d05cddcSAtari911            tooltip.style.borderColor = "#00cc07";
12631d05cddcSAtari911            tooltip.style.color = "#00cc07";
12641d05cddcSAtari911        } else if (color === "purple") {
12651d05cddcSAtari911            // Purple bar: Load averages (short-term) and top processes
12661d05cddcSAtari911            content = "<div class=\"tooltip-title\">CPU Load (Short-term)</div>";
12671d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
12681d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
12691d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
12701d05cddcSAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(155,89,182,0.3);\" class=\"tooltip-title\">Top Processes</div>";
12711d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
12721d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
12731d05cddcSAtari911                });
12741d05cddcSAtari911            }
12751d05cddcSAtari911            tooltip.style.borderColor = "#9b59b6";
12761d05cddcSAtari911            tooltip.style.color = "#9b59b6";
12771d05cddcSAtari911        } else if (color === "orange") {
12781d05cddcSAtari911            // Orange bar: Memory details and top processes
12791d05cddcSAtari911            content = "<div class=\"tooltip-title\">Memory Usage</div>";
12801d05cddcSAtari911            if (latestStats.memory_details && latestStats.memory_details.total) {
12811d05cddcSAtari911                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
12821d05cddcSAtari911                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
12831d05cddcSAtari911                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
12841d05cddcSAtari911                if (latestStats.memory_details.cached) {
12851d05cddcSAtari911                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
12861d05cddcSAtari911                }
12871d05cddcSAtari911            } else {
12881d05cddcSAtari911                content += "<div>Loading...</div>";
12891d05cddcSAtari911            }
12901d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
12911d05cddcSAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(255,140,0,0.3);\" class=\"tooltip-title\">Top Processes</div>";
12921d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
12931d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
12941d05cddcSAtari911                });
12951d05cddcSAtari911            }
12961d05cddcSAtari911            tooltip.style.borderColor = "#ff9800";
12971d05cddcSAtari911            tooltip.style.color = "#ff9800";
12981d05cddcSAtari911        }
12991d05cddcSAtari911
13001d05cddcSAtari911        console.log("Tooltip content:", content);
13011d05cddcSAtari911        tooltip.innerHTML = content;
13021d05cddcSAtari911        tooltip.style.display = "block";
13031d05cddcSAtari911
13041d05cddcSAtari911        // Position tooltip using fixed positioning above the bar
13051d05cddcSAtari911        const bar = tooltip.parentElement;
13061d05cddcSAtari911        const barRect = bar.getBoundingClientRect();
13071d05cddcSAtari911        const tooltipRect = tooltip.getBoundingClientRect();
13081d05cddcSAtari911
13091d05cddcSAtari911        // Center horizontally on the bar
13101d05cddcSAtari911        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
13111d05cddcSAtari911        // Position above the bar with 8px gap
13121d05cddcSAtari911        const top = barRect.top - tooltipRect.height - 8;
13131d05cddcSAtari911
13141d05cddcSAtari911        tooltip.style.left = left + "px";
13151d05cddcSAtari911        tooltip.style.top = top + "px";
13161d05cddcSAtari911    };
13171d05cddcSAtari911
13181d05cddcSAtari911    window["hideTooltip_' . $calId . '"] = function(color) {
13191d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
13201d05cddcSAtari911        if (tooltip) {
13211d05cddcSAtari911            tooltip.style.display = "none";
13221d05cddcSAtari911        }
13231d05cddcSAtari911    };
13241d05cddcSAtari911
13251d05cddcSAtari911    // Update CPU and memory bars every 2 seconds
13261d05cddcSAtari911    function updateSystemStats() {
13271d05cddcSAtari911        // Fetch real system stats from server
13281d05cddcSAtari911        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
13291d05cddcSAtari911            .then(response => response.json())
13301d05cddcSAtari911            .then(data => {
13311d05cddcSAtari911                console.log("System stats received:", data);
13321d05cddcSAtari911
13331d05cddcSAtari911                // Store data for tooltips
13341d05cddcSAtari911                latestStats = {
13351d05cddcSAtari911                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
13361d05cddcSAtari911                    uptime: data.uptime || "",
13371d05cddcSAtari911                    memory_details: data.memory_details || {},
13381d05cddcSAtari911                    top_processes: data.top_processes || []
13391d05cddcSAtari911                };
13401d05cddcSAtari911
13411d05cddcSAtari911                console.log("latestStats updated to:", latestStats);
13421d05cddcSAtari911
13431d05cddcSAtari911                // Update green bar (5-minute average) - updates live now!
13441d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
13451d05cddcSAtari911                if (greenBar) {
13461d05cddcSAtari911                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
13471d05cddcSAtari911                }
13481d05cddcSAtari911
13491d05cddcSAtari911                // Add current CPU to history for purple bar
13501d05cddcSAtari911                cpuHistory.push(data.cpu);
13511d05cddcSAtari911                if (cpuHistory.length > CPU_HISTORY_SIZE) {
13521d05cddcSAtari911                    cpuHistory.shift(); // Remove oldest
13531d05cddcSAtari911                }
13541d05cddcSAtari911
13551d05cddcSAtari911                // Calculate 5-second average for CPU
13561d05cddcSAtari911                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
13571d05cddcSAtari911
13581d05cddcSAtari911                // Update CPU bar (purple) with 5-second average
13591d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
13601d05cddcSAtari911                if (cpuBar) {
13611d05cddcSAtari911                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
13621d05cddcSAtari911                }
13631d05cddcSAtari911
13641d05cddcSAtari911                // Update memory bar (orange) with real data
13651d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
13661d05cddcSAtari911                if (memBar) {
13671d05cddcSAtari911                    memBar.style.width = Math.min(100, data.memory) + "%";
13681d05cddcSAtari911                }
13691d05cddcSAtari911            })
13701d05cddcSAtari911            .catch(error => {
13711d05cddcSAtari911                console.log("System stats error:", error);
13721d05cddcSAtari911                // Fallback to client-side estimates on error
13731d05cddcSAtari911                const cpuFallback = Math.random() * 100;
13741d05cddcSAtari911                cpuHistory.push(cpuFallback);
13751d05cddcSAtari911                if (cpuHistory.length > CPU_HISTORY_SIZE) {
13761d05cddcSAtari911                    cpuHistory.shift();
13771d05cddcSAtari911                }
13781d05cddcSAtari911                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
13791d05cddcSAtari911
13801d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
13811d05cddcSAtari911                if (greenBar) greenBar.style.width = Math.min(100, cpuFallback) + "%";
13821d05cddcSAtari911
13831d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
13841d05cddcSAtari911                if (cpuBar) cpuBar.style.width = Math.min(100, cpuAverage) + "%";
13851d05cddcSAtari911
13861d05cddcSAtari911                let memoryUsage = 0;
13871d05cddcSAtari911                if (performance.memory) {
13881d05cddcSAtari911                    memoryUsage = (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100;
13891d05cddcSAtari911                } else {
13901d05cddcSAtari911                    memoryUsage = Math.random() * 100;
13911d05cddcSAtari911                }
13921d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
13931d05cddcSAtari911                if (memBar) memBar.style.width = Math.min(100, memoryUsage) + "%";
13941d05cddcSAtari911            });
13951d05cddcSAtari911    }
13961d05cddcSAtari911
13971d05cddcSAtari911    // Update immediately and then every 2 seconds
13981d05cddcSAtari911    updateSystemStats();
13991d05cddcSAtari911    setInterval(updateSystemStats, 2000);
14001d05cddcSAtari911})();
14011d05cddcSAtari911</script>';
14021d05cddcSAtari911        }
140319378907SAtari911
140419378907SAtari911        if (empty($allEvents)) {
1405e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-empty">';
1406e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText);
1407e3a9f44cSAtari911            if ($namespace) {
1408e3a9f44cSAtari911                $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>';
140987ac9bf3SAtari911            }
1410e3a9f44cSAtari911            $html .= '</div>';
1411e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-body">No events</div>';
1412e3a9f44cSAtari911            $html .= '</div>';
1413e3a9f44cSAtari911        } else {
1414e3a9f44cSAtari911            // Calculate today and tomorrow's dates for highlighting
14151d05cddcSAtari911            $todayStr = date('Y-m-d');
1416e3a9f44cSAtari911            $tomorrow = date('Y-m-d', strtotime('+1 day'));
1417e3a9f44cSAtari911
1418e3a9f44cSAtari911            foreach ($allEvents as $dateKey => $dayEvents) {
1419e3a9f44cSAtari911                $dateObj = new DateTime($dateKey);
1420e3a9f44cSAtari911                $displayDate = $dateObj->format('D, M j');
1421e3a9f44cSAtari911
14221d05cddcSAtari911                // Check if this date is today or tomorrow or past
1423e3a9f44cSAtari911                // Enable highlighting for sidebar mode AND range modes (day, week, month)
1424e3a9f44cSAtari911                $enableHighlighting = $sidebar || !empty($range);
14251d05cddcSAtari911                $isToday = $enableHighlighting && ($dateKey === $todayStr);
1426e3a9f44cSAtari911                $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow);
14271d05cddcSAtari911                $isPast = $dateKey < $todayStr;
142819378907SAtari911
142919378907SAtari911                foreach ($dayEvents as $event) {
14301d05cddcSAtari911                    // Check if this is a task and if it's completed
14311d05cddcSAtari911                    $isTask = !empty($event['isTask']);
14321d05cddcSAtari911                    $completed = !empty($event['completed']);
14331d05cddcSAtari911
14341d05cddcSAtari911                    // ALWAYS skip completed tasks UNLESS showchecked is explicitly set
14351d05cddcSAtari911                    if (!$showchecked && $isTask && $completed) {
1436e3a9f44cSAtari911                        continue;
1437e3a9f44cSAtari911                    }
143819378907SAtari911
14391d05cddcSAtari911                    // Skip past events that are NOT tasks (only show past due tasks from the past)
14401d05cddcSAtari911                    if ($isPast && !$isTask) {
14411d05cddcSAtari911                        continue;
14421d05cddcSAtari911                    }
14431d05cddcSAtari911
14441d05cddcSAtari911                    // Determine if task is past due (past date, is task, not completed)
14451d05cddcSAtari911                    $isPastDue = $isPast && $isTask && !$completed;
14461d05cddcSAtari911
1447e3a9f44cSAtari911                    // Line 1: Header (Title, Time, Date, Namespace)
1448e3a9f44cSAtari911                    $todayClass = $isToday ? ' eventlist-simple-today' : '';
1449e3a9f44cSAtari911                    $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : '';
14501d05cddcSAtari911                    $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : '';
14511d05cddcSAtari911                    $html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . $pastDueClass . '">';
1452e3a9f44cSAtari911                    $html .= '<div class="eventlist-simple-header">';
1453e3a9f44cSAtari911
1454e3a9f44cSAtari911                    // Title
1455e3a9f44cSAtari911                    $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>';
1456e3a9f44cSAtari911
1457e3a9f44cSAtari911                    // Time (12-hour format)
1458e3a9f44cSAtari911                    if (!empty($event['time'])) {
1459e3a9f44cSAtari911                        $timeParts = explode(':', $event['time']);
146087ac9bf3SAtari911                        if (count($timeParts) === 2) {
146187ac9bf3SAtari911                            $hour = (int)$timeParts[0];
146287ac9bf3SAtari911                            $minute = $timeParts[1];
146387ac9bf3SAtari911                            $ampm = $hour >= 12 ? 'PM' : 'AM';
1464e3a9f44cSAtari911                            $hour = $hour % 12 ?: 12;
146587ac9bf3SAtari911                            $displayTime = $hour . ':' . $minute . ' ' . $ampm;
1466e3a9f44cSAtari911                            $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>';
146719378907SAtari911                        }
146887ac9bf3SAtari911                    }
146987ac9bf3SAtari911
1470e3a9f44cSAtari911                    // Date
1471e3a9f44cSAtari911                    $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>';
1472e3a9f44cSAtari911
14731d05cddcSAtari911                    // Badge: PAST DUE, TODAY, or nothing
14741d05cddcSAtari911                    if ($isPastDue) {
14751d05cddcSAtari911                        $html .= ' <span class="eventlist-simple-pastdue-badge">PAST DUE</span>';
14761d05cddcSAtari911                    } elseif ($isToday) {
1477e3a9f44cSAtari911                        $html .= ' <span class="eventlist-simple-today-badge">TODAY</span>';
147887ac9bf3SAtari911                    }
1479e3a9f44cSAtari911
1480e3a9f44cSAtari911                    // Namespace badge (show individual event's namespace)
1481e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
1482e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
1483e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading
148419378907SAtari911                    }
1485e3a9f44cSAtari911                    if ($eventNamespace) {
1486e3a9f44cSAtari911                        $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</span>';
1487e3a9f44cSAtari911                    }
1488e3a9f44cSAtari911
1489e3a9f44cSAtari911                    $html .= '</div>'; // header
1490e3a9f44cSAtari911
1491e3a9f44cSAtari911                    // Line 2: Body (Description only) - only show if description exists
1492e3a9f44cSAtari911                    if (!empty($event['description'])) {
1493e3a9f44cSAtari911                        $html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) . '</div>';
1494e3a9f44cSAtari911                    }
1495e3a9f44cSAtari911
1496e3a9f44cSAtari911                    $html .= '</div>'; // item
149719378907SAtari911                }
149819378907SAtari911            }
149987ac9bf3SAtari911        }
150019378907SAtari911
1501e3a9f44cSAtari911        $html .= '</div>'; // eventlist-simple
150219378907SAtari911
150319378907SAtari911        return $html;
150419378907SAtari911    }
150519378907SAtari911
150619378907SAtari911    private function renderEventDialog($calId, $namespace) {
1507*9ccd446eSAtari911        // Get theme for dialog
1508*9ccd446eSAtari911        $theme = $this->getSidebarTheme();
1509*9ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
1510*9ccd446eSAtari911
151119378907SAtari911        $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">';
151219378907SAtari911        $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>';
151319378907SAtari911
1514*9ccd446eSAtari911        // Draggable dialog with theme
151519378907SAtari911        $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">';
151619378907SAtari911
151719378907SAtari911        // Header with drag handle and close button
151819378907SAtari911        $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">';
151919378907SAtari911        $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>';
152019378907SAtari911        $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>';
152119378907SAtari911        $html .= '</div>';
152219378907SAtari911
152319378907SAtari911        // Form content
152419378907SAtari911        $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">';
152519378907SAtari911
152619378907SAtari911        // Hidden ID field
152719378907SAtari911        $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">';
152819378907SAtari911
15291d05cddcSAtari911        // 1. TITLE
15301d05cddcSAtari911        $html .= '<div class="form-field">';
15311d05cddcSAtari911        $html .= '<label class="field-label">�� Title</label>';
15321d05cddcSAtari911        $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek input-compact" placeholder="Event or task title...">';
153319378907SAtari911        $html .= '</div>';
153419378907SAtari911
15351d05cddcSAtari911        // 1.5 NAMESPACE SELECTOR (Searchable with fuzzy matching)
15361d05cddcSAtari911        $html .= '<div class="form-field">';
15371d05cddcSAtari911        $html .= '<label class="field-label">�� Namespace</label>';
15381d05cddcSAtari911
15391d05cddcSAtari911        // Hidden field to store actual selected namespace
15401d05cddcSAtari911        $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">';
15411d05cddcSAtari911
15421d05cddcSAtari911        // Searchable input
15431d05cddcSAtari911        $html .= '<div class="namespace-search-wrapper">';
15441d05cddcSAtari911        $html .= '<input type="text" id="event-namespace-search-' . $calId . '" class="input-sleek input-compact namespace-search-input" placeholder="Type to search or leave empty for default..." autocomplete="off">';
15451d05cddcSAtari911        $html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="display:none;"></div>';
15461d05cddcSAtari911        $html .= '</div>';
15471d05cddcSAtari911
15481d05cddcSAtari911        // Store namespaces as JSON for JavaScript
15491d05cddcSAtari911        $allNamespaces = $this->getAllNamespaces();
15501d05cddcSAtari911        $html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($allNamespaces) . '</script>';
15511d05cddcSAtari911
15521d05cddcSAtari911        $html .= '</div>';
15531d05cddcSAtari911
15541d05cddcSAtari911        // 2. DESCRIPTION
15551d05cddcSAtari911        $html .= '<div class="form-field">';
15561d05cddcSAtari911        $html .= '<label class="field-label">�� Description</label>';
1557*9ccd446eSAtari911        $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="2" class="input-sleek textarea-sleek textarea-compact" placeholder="Optional details..."></textarea>';
15581d05cddcSAtari911        $html .= '</div>';
15591d05cddcSAtari911
15601d05cddcSAtari911        // 3. START DATE - END DATE (inline)
156119378907SAtari911        $html .= '<div class="form-row-group">';
156219378907SAtari911
15631d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
15641d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Start Date</label>';
15651d05cddcSAtari911        $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date input-compact">';
156619378907SAtari911        $html .= '</div>';
156719378907SAtari911
15681d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
15691d05cddcSAtari911        $html .= '<label class="field-label-compact">�� End Date</label>';
15701d05cddcSAtari911        $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date input-compact" placeholder="Optional">';
157119378907SAtari911        $html .= '</div>';
157219378907SAtari911
15731d05cddcSAtari911        $html .= '</div>'; // End row
157419378907SAtari911
15751d05cddcSAtari911        // 4. IS REPEATING CHECKBOX
15761d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
15771d05cddcSAtari911        $html .= '<label class="checkbox-label checkbox-label-compact">';
157887ac9bf3SAtari911        $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">';
157987ac9bf3SAtari911        $html .= '<span>�� Repeating Event</span>';
158087ac9bf3SAtari911        $html .= '</label>';
158187ac9bf3SAtari911        $html .= '</div>';
158287ac9bf3SAtari911
15831d05cddcSAtari911        // Recurring options (shown when checkbox is checked)
158487ac9bf3SAtari911        $html .= '<div id="recurring-options-' . $calId . '" class="recurring-options" style="display:none;">';
158587ac9bf3SAtari911
15861d05cddcSAtari911        $html .= '<div class="form-row-group">';
15871d05cddcSAtari911
15881d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
15891d05cddcSAtari911        $html .= '<label class="field-label-compact">Repeat Every</label>';
15901d05cddcSAtari911        $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek input-compact">';
159187ac9bf3SAtari911        $html .= '<option value="daily">Daily</option>';
159287ac9bf3SAtari911        $html .= '<option value="weekly">Weekly</option>';
159387ac9bf3SAtari911        $html .= '<option value="monthly">Monthly</option>';
159487ac9bf3SAtari911        $html .= '<option value="yearly">Yearly</option>';
159587ac9bf3SAtari911        $html .= '</select>';
159687ac9bf3SAtari911        $html .= '</div>';
159787ac9bf3SAtari911
15981d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
15991d05cddcSAtari911        $html .= '<label class="field-label-compact">Repeat Until</label>';
16001d05cddcSAtari911        $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date input-compact" placeholder="Optional">';
160187ac9bf3SAtari911        $html .= '</div>';
160287ac9bf3SAtari911
16031d05cddcSAtari911        $html .= '</div>'; // End row
16041d05cddcSAtari911        $html .= '</div>'; // End recurring options
160587ac9bf3SAtari911
16061d05cddcSAtari911        // 5. TIME (Start & End) - COLOR (inline)
16071d05cddcSAtari911        $html .= '<div class="form-row-group">';
16081d05cddcSAtari911
16091d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
16101d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Start Time</label>';
16111d05cddcSAtari911        $html .= '<select id="event-time-' . $calId . '" name="time" class="input-sleek input-compact" onchange="updateEndTimeOptions(\'' . $calId . '\')">';
16121d05cddcSAtari911        $html .= '<option value="">All day</option>';
1613e3a9f44cSAtari911
1614e3a9f44cSAtari911        // Generate time options in 15-minute intervals
1615e3a9f44cSAtari911        for ($hour = 0; $hour < 24; $hour++) {
1616e3a9f44cSAtari911            for ($minute = 0; $minute < 60; $minute += 15) {
1617e3a9f44cSAtari911                $timeValue = sprintf('%02d:%02d', $hour, $minute);
1618e3a9f44cSAtari911                $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
1619e3a9f44cSAtari911                $ampm = $hour < 12 ? 'AM' : 'PM';
1620e3a9f44cSAtari911                $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
1621e3a9f44cSAtari911                $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
1622e3a9f44cSAtari911            }
1623e3a9f44cSAtari911        }
1624e3a9f44cSAtari911
1625e3a9f44cSAtari911        $html .= '</select>';
162619378907SAtari911        $html .= '</div>';
162719378907SAtari911
16281d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
16291d05cddcSAtari911        $html .= '<label class="field-label-compact">�� End Time</label>';
16301d05cddcSAtari911        $html .= '<select id="event-end-time-' . $calId . '" name="endTime" class="input-sleek input-compact">';
16311d05cddcSAtari911        $html .= '<option value="">Same as start</option>';
16321d05cddcSAtari911
16331d05cddcSAtari911        // Generate time options in 15-minute intervals
16341d05cddcSAtari911        for ($hour = 0; $hour < 24; $hour++) {
16351d05cddcSAtari911            for ($minute = 0; $minute < 60; $minute += 15) {
16361d05cddcSAtari911                $timeValue = sprintf('%02d:%02d', $hour, $minute);
16371d05cddcSAtari911                $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
16381d05cddcSAtari911                $ampm = $hour < 12 ? 'AM' : 'PM';
16391d05cddcSAtari911                $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
16401d05cddcSAtari911                $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
16411d05cddcSAtari911            }
16421d05cddcSAtari911        }
16431d05cddcSAtari911
16441d05cddcSAtari911        $html .= '</select>';
164519378907SAtari911        $html .= '</div>';
164619378907SAtari911
16471d05cddcSAtari911        $html .= '</div>'; // End row
16481d05cddcSAtari911
16491d05cddcSAtari911        // Color field (new row)
16501d05cddcSAtari911        $html .= '<div class="form-row-group">';
16511d05cddcSAtari911
16521d05cddcSAtari911        $html .= '<div class="form-field form-field-full">';
16531d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Color</label>';
16541d05cddcSAtari911        $html .= '<div class="color-picker-wrapper">';
16551d05cddcSAtari911        $html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact color-select" onchange="updateCustomColorPicker(\'' . $calId . '\')">';
16561d05cddcSAtari911        $html .= '<option value="#3498db" style="background:#3498db;color:white">�� Blue</option>';
16571d05cddcSAtari911        $html .= '<option value="#2ecc71" style="background:#2ecc71;color:white">�� Green</option>';
16581d05cddcSAtari911        $html .= '<option value="#e74c3c" style="background:#e74c3c;color:white">�� Red</option>';
16591d05cddcSAtari911        $html .= '<option value="#f39c12" style="background:#f39c12;color:white">�� Orange</option>';
16601d05cddcSAtari911        $html .= '<option value="#9b59b6" style="background:#9b59b6;color:white">�� Purple</option>';
16611d05cddcSAtari911        $html .= '<option value="#e91e63" style="background:#e91e63;color:white">�� Pink</option>';
16621d05cddcSAtari911        $html .= '<option value="#1abc9c" style="background:#1abc9c;color:white">�� Teal</option>';
16631d05cddcSAtari911        $html .= '<option value="custom">�� Custom...</option>';
16641d05cddcSAtari911        $html .= '</select>';
16651d05cddcSAtari911        $html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input color-picker-compact" value="#3498db" onchange="updateColorFromPicker(\'' . $calId . '\')">';
16661d05cddcSAtari911        $html .= '</div>';
166719378907SAtari911        $html .= '</div>';
166819378907SAtari911
16691d05cddcSAtari911        $html .= '</div>'; // End row
16701d05cddcSAtari911
16711d05cddcSAtari911        // Task checkbox
16721d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
16731d05cddcSAtari911        $html .= '<label class="checkbox-label checkbox-label-compact">';
16741d05cddcSAtari911        $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">';
16751d05cddcSAtari911        $html .= '<span>�� This is a task (can be checked off)</span>';
16761d05cddcSAtari911        $html .= '</label>';
167719378907SAtari911        $html .= '</div>';
167819378907SAtari911
167919378907SAtari911        // Action buttons
168019378907SAtari911        $html .= '<div class="dialog-actions-sleek">';
168119378907SAtari911        $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>';
168219378907SAtari911        $html .= '<button type="submit" class="btn-sleek btn-save-sleek">�� Save</button>';
168319378907SAtari911        $html .= '</div>';
168419378907SAtari911
168519378907SAtari911        $html .= '</form>';
168619378907SAtari911        $html .= '</div>';
168719378907SAtari911        $html .= '</div>';
168819378907SAtari911
168919378907SAtari911        return $html;
169019378907SAtari911    }
169119378907SAtari911
1692*9ccd446eSAtari911    private function renderMonthPicker($calId, $year, $month, $namespace, $theme = 'matrix', $themeStyles = null) {
1693*9ccd446eSAtari911        // Fallback to default theme if not provided
1694*9ccd446eSAtari911        if ($themeStyles === null) {
1695*9ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
1696*9ccd446eSAtari911        }
1697*9ccd446eSAtari911
1698*9ccd446eSAtari911        $themeClass = 'calendar-theme-' . $theme;
1699*9ccd446eSAtari911
1700*9ccd446eSAtari911        $html = '<div class="month-picker-overlay ' . $themeClass . '" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">';
170187ac9bf3SAtari911        $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">';
170287ac9bf3SAtari911        $html .= '<h4>Jump to Month</h4>';
170387ac9bf3SAtari911
170487ac9bf3SAtari911        $html .= '<div class="month-picker-selects">';
170587ac9bf3SAtari911        $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">';
170687ac9bf3SAtari911        $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
170787ac9bf3SAtari911        for ($m = 1; $m <= 12; $m++) {
170887ac9bf3SAtari911            $selected = ($m == $month) ? ' selected' : '';
170987ac9bf3SAtari911            $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>';
171087ac9bf3SAtari911        }
171187ac9bf3SAtari911        $html .= '</select>';
171287ac9bf3SAtari911
171387ac9bf3SAtari911        $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">';
171487ac9bf3SAtari911        $currentYear = (int)date('Y');
171587ac9bf3SAtari911        for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) {
171687ac9bf3SAtari911            $selected = ($y == $year) ? ' selected' : '';
171787ac9bf3SAtari911            $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>';
171887ac9bf3SAtari911        }
171987ac9bf3SAtari911        $html .= '</select>';
172087ac9bf3SAtari911        $html .= '</div>';
172187ac9bf3SAtari911
172287ac9bf3SAtari911        $html .= '<div class="month-picker-actions">';
172387ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>';
172487ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>';
172587ac9bf3SAtari911        $html .= '</div>';
172687ac9bf3SAtari911
172787ac9bf3SAtari911        $html .= '</div>';
172887ac9bf3SAtari911        $html .= '</div>';
172987ac9bf3SAtari911
173087ac9bf3SAtari911        return $html;
173187ac9bf3SAtari911    }
173287ac9bf3SAtari911
1733*9ccd446eSAtari911    private function renderDescription($description, $themeStyles = null) {
173419378907SAtari911        if (empty($description)) {
173519378907SAtari911            return '';
173619378907SAtari911        }
173719378907SAtari911
1738*9ccd446eSAtari911        // Get theme for link colors if not provided
1739*9ccd446eSAtari911        if ($themeStyles === null) {
1740*9ccd446eSAtari911            $theme = $this->getSidebarTheme();
1741*9ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
1742*9ccd446eSAtari911        }
1743*9ccd446eSAtari911
1744*9ccd446eSAtari911        $linkColor = '';
1745*9ccd446eSAtari911        $linkStyle = ' class="cal-link"';
1746*9ccd446eSAtari911
1747e3a9f44cSAtari911        // Token-based parsing to avoid escaping issues
1748e3a9f44cSAtari911        $rendered = $description;
1749e3a9f44cSAtari911        $tokens = array();
1750e3a9f44cSAtari911        $tokenIndex = 0;
175119378907SAtari911
1752e3a9f44cSAtari911        // Convert DokuWiki image syntax {{image.jpg}} to tokens
1753e3a9f44cSAtari911        $pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/';
1754e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1755e3a9f44cSAtari911        foreach ($matches as $match) {
1756e3a9f44cSAtari911            $imagePath = trim($match[1]);
1757e3a9f44cSAtari911            $alt = isset($match[2]) ? trim($match[2]) : '';
175819378907SAtari911
1759e3a9f44cSAtari911            // Handle external URLs
176019378907SAtari911            if (preg_match('/^https?:\/\//', $imagePath)) {
1761e3a9f44cSAtari911                $imageHtml = '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
1762e3a9f44cSAtari911            } else {
176319378907SAtari911                // Handle internal DokuWiki images
176419378907SAtari911                $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath);
1765e3a9f44cSAtari911                $imageHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
1766e3a9f44cSAtari911            }
176719378907SAtari911
1768e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
1769e3a9f44cSAtari911            $tokens[$tokenIndex] = $imageHtml;
1770e3a9f44cSAtari911            $tokenIndex++;
1771e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
1772e3a9f44cSAtari911        }
1773e3a9f44cSAtari911
1774e3a9f44cSAtari911        // Convert DokuWiki link syntax [[link|text]] to tokens
1775e3a9f44cSAtari911        $pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/';
1776e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1777e3a9f44cSAtari911        foreach ($matches as $match) {
1778e3a9f44cSAtari911            $link = trim($match[1]);
1779e3a9f44cSAtari911            $text = isset($match[2]) ? trim($match[2]) : $link;
178019378907SAtari911
178119378907SAtari911            // Handle external URLs
178219378907SAtari911            if (preg_match('/^https?:\/\//', $link)) {
1783*9ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
1784e3a9f44cSAtari911            } else {
178587ac9bf3SAtari911                // Handle internal DokuWiki links with section anchors
178687ac9bf3SAtari911                $parts = explode('#', $link, 2);
178787ac9bf3SAtari911                $pagePart = $parts[0];
178887ac9bf3SAtari911                $sectionPart = isset($parts[1]) ? '#' . $parts[1] : '';
178987ac9bf3SAtari911
179087ac9bf3SAtari911                $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart;
1791*9ccd446eSAtari911                $linkHtml = '<a href="' . $wikiUrl . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
179219378907SAtari911            }
179319378907SAtari911
1794e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
1795e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
1796e3a9f44cSAtari911            $tokenIndex++;
1797e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
1798e3a9f44cSAtari911        }
179919378907SAtari911
1800e3a9f44cSAtari911        // Convert markdown-style links [text](url) to tokens
1801e3a9f44cSAtari911        $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
1802e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1803e3a9f44cSAtari911        foreach ($matches as $match) {
1804e3a9f44cSAtari911            $text = trim($match[1]);
1805e3a9f44cSAtari911            $url = trim($match[2]);
180619378907SAtari911
1807e3a9f44cSAtari911            if (preg_match('/^https?:\/\//', $url)) {
1808*9ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
1809e3a9f44cSAtari911            } else {
1810*9ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
1811e3a9f44cSAtari911            }
1812e3a9f44cSAtari911
1813e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
1814e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
1815e3a9f44cSAtari911            $tokenIndex++;
1816e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
1817e3a9f44cSAtari911        }
1818e3a9f44cSAtari911
1819e3a9f44cSAtari911        // Convert plain URLs to tokens
1820e3a9f44cSAtari911        $pattern = '/(https?:\/\/[^\s<]+)/';
1821e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1822e3a9f44cSAtari911        foreach ($matches as $match) {
1823e3a9f44cSAtari911            $url = $match[1];
1824*9ccd446eSAtari911            $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($url) . '</a>';
1825e3a9f44cSAtari911
1826e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
1827e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
1828e3a9f44cSAtari911            $tokenIndex++;
1829e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
1830e3a9f44cSAtari911        }
1831e3a9f44cSAtari911
1832e3a9f44cSAtari911        // NOW escape HTML (tokens are protected)
1833e3a9f44cSAtari911        $rendered = htmlspecialchars($rendered);
1834e3a9f44cSAtari911
1835e3a9f44cSAtari911        // Convert newlines to <br>
1836e3a9f44cSAtari911        $rendered = nl2br($rendered);
1837e3a9f44cSAtari911
1838e3a9f44cSAtari911        // DokuWiki text formatting
1839e3a9f44cSAtari911        // Bold: **text** or __text__
1840*9ccd446eSAtari911        $boldStyle = '';
1841e3a9f44cSAtari911        $rendered = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $rendered);
1842e3a9f44cSAtari911        $rendered = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $rendered);
1843e3a9f44cSAtari911
1844e3a9f44cSAtari911        // Italic: //text//
1845e3a9f44cSAtari911        $rendered = preg_replace('/\/\/(.+?)\/\//', '<em>$1</em>', $rendered);
1846e3a9f44cSAtari911
1847e3a9f44cSAtari911        // Strikethrough: <del>text</del>
1848e3a9f44cSAtari911        $rendered = preg_replace('/&lt;del&gt;(.+?)&lt;\/del&gt;/', '<del>$1</del>', $rendered);
1849e3a9f44cSAtari911
1850e3a9f44cSAtari911        // Monospace: ''text''
1851e3a9f44cSAtari911        $rendered = preg_replace('/&#039;&#039;(.+?)&#039;&#039;/', '<code>$1</code>', $rendered);
1852e3a9f44cSAtari911
1853e3a9f44cSAtari911        // Subscript: <sub>text</sub>
1854e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sub&gt;(.+?)&lt;\/sub&gt;/', '<sub>$1</sub>', $rendered);
1855e3a9f44cSAtari911
1856e3a9f44cSAtari911        // Superscript: <sup>text</sup>
1857e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sup&gt;(.+?)&lt;\/sup&gt;/', '<sup>$1</sup>', $rendered);
1858e3a9f44cSAtari911
1859e3a9f44cSAtari911        // Restore tokens
1860e3a9f44cSAtari911        foreach ($tokens as $i => $html) {
1861e3a9f44cSAtari911            $rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered);
1862e3a9f44cSAtari911        }
186319378907SAtari911
186419378907SAtari911        return $rendered;
186519378907SAtari911    }
186619378907SAtari911
186719378907SAtari911    private function loadEvents($namespace, $year, $month) {
186819378907SAtari911        $dataDir = DOKU_INC . 'data/meta/';
186919378907SAtari911        if ($namespace) {
187019378907SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
187119378907SAtari911        }
187219378907SAtari911        $dataDir .= 'calendar/';
187319378907SAtari911
187419378907SAtari911        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
187519378907SAtari911
187619378907SAtari911        if (file_exists($eventFile)) {
187719378907SAtari911            $json = file_get_contents($eventFile);
187819378907SAtari911            return json_decode($json, true);
187919378907SAtari911        }
188019378907SAtari911
188119378907SAtari911        return array();
188219378907SAtari911    }
1883e3a9f44cSAtari911
1884e3a9f44cSAtari911    private function loadEventsMultiNamespace($namespaces, $year, $month) {
1885e3a9f44cSAtari911        // Check for wildcard pattern (namespace:*)
1886e3a9f44cSAtari911        if (preg_match('/^(.+):\*$/', $namespaces, $matches)) {
1887e3a9f44cSAtari911            $baseNamespace = $matches[1];
1888e3a9f44cSAtari911            return $this->loadEventsWildcard($baseNamespace, $year, $month);
1889e3a9f44cSAtari911        }
1890e3a9f44cSAtari911
1891e3a9f44cSAtari911        // Check for root wildcard (just *)
1892e3a9f44cSAtari911        if ($namespaces === '*') {
1893e3a9f44cSAtari911            return $this->loadEventsWildcard('', $year, $month);
1894e3a9f44cSAtari911        }
1895e3a9f44cSAtari911
1896e3a9f44cSAtari911        // Parse namespace list (semicolon separated)
1897e3a9f44cSAtari911        // e.g., "team:projects;personal;work:tasks" = three namespaces
1898e3a9f44cSAtari911        $namespaceList = array_map('trim', explode(';', $namespaces));
1899e3a9f44cSAtari911
1900e3a9f44cSAtari911        // Load events from all namespaces
1901e3a9f44cSAtari911        $allEvents = array();
1902e3a9f44cSAtari911        foreach ($namespaceList as $ns) {
1903e3a9f44cSAtari911            $ns = trim($ns);
1904e3a9f44cSAtari911            if (empty($ns)) continue;
1905e3a9f44cSAtari911
1906e3a9f44cSAtari911            $events = $this->loadEvents($ns, $year, $month);
1907e3a9f44cSAtari911
1908e3a9f44cSAtari911            // Add namespace tag to each event
1909e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
1910e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
1911e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
1912e3a9f44cSAtari911                }
1913e3a9f44cSAtari911                foreach ($dayEvents as $event) {
1914e3a9f44cSAtari911                    $event['_namespace'] = $ns;
1915e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
1916e3a9f44cSAtari911                }
1917e3a9f44cSAtari911            }
1918e3a9f44cSAtari911        }
1919e3a9f44cSAtari911
1920e3a9f44cSAtari911        return $allEvents;
1921e3a9f44cSAtari911    }
1922e3a9f44cSAtari911
1923e3a9f44cSAtari911    private function loadEventsWildcard($baseNamespace, $year, $month) {
1924e3a9f44cSAtari911        // Find all subdirectories under the base namespace
1925e3a9f44cSAtari911        $dataDir = DOKU_INC . 'data/meta/';
1926e3a9f44cSAtari911        if ($baseNamespace) {
1927e3a9f44cSAtari911            $dataDir .= str_replace(':', '/', $baseNamespace) . '/';
1928e3a9f44cSAtari911        }
1929e3a9f44cSAtari911
1930e3a9f44cSAtari911        $allEvents = array();
1931e3a9f44cSAtari911
1932e3a9f44cSAtari911        // First, load events from the base namespace itself
1933e3a9f44cSAtari911        if (empty($baseNamespace)) {
1934e3a9f44cSAtari911            // Root wildcard - load from root calendar
1935e3a9f44cSAtari911            $events = $this->loadEvents('', $year, $month);
1936e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
1937e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
1938e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
1939e3a9f44cSAtari911                }
1940e3a9f44cSAtari911                foreach ($dayEvents as $event) {
1941e3a9f44cSAtari911                    $event['_namespace'] = '';
1942e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
1943e3a9f44cSAtari911                }
1944e3a9f44cSAtari911            }
1945e3a9f44cSAtari911        } else {
1946e3a9f44cSAtari911            $events = $this->loadEvents($baseNamespace, $year, $month);
1947e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
1948e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
1949e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
1950e3a9f44cSAtari911                }
1951e3a9f44cSAtari911                foreach ($dayEvents as $event) {
1952e3a9f44cSAtari911                    $event['_namespace'] = $baseNamespace;
1953e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
1954e3a9f44cSAtari911                }
1955e3a9f44cSAtari911            }
1956e3a9f44cSAtari911        }
1957e3a9f44cSAtari911
1958e3a9f44cSAtari911        // Recursively find all subdirectories
1959e3a9f44cSAtari911        $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents);
1960e3a9f44cSAtari911
1961e3a9f44cSAtari911        return $allEvents;
1962e3a9f44cSAtari911    }
1963e3a9f44cSAtari911
1964e3a9f44cSAtari911    private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) {
1965e3a9f44cSAtari911        if (!is_dir($dir)) return;
1966e3a9f44cSAtari911
1967e3a9f44cSAtari911        $items = scandir($dir);
1968e3a9f44cSAtari911        foreach ($items as $item) {
1969e3a9f44cSAtari911            if ($item === '.' || $item === '..') continue;
1970e3a9f44cSAtari911
1971e3a9f44cSAtari911            $path = $dir . $item;
1972e3a9f44cSAtari911            if (is_dir($path) && $item !== 'calendar') {
1973e3a9f44cSAtari911                // This is a namespace directory
1974e3a9f44cSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
1975e3a9f44cSAtari911
1976e3a9f44cSAtari911                // Load events from this namespace
1977e3a9f44cSAtari911                $events = $this->loadEvents($namespace, $year, $month);
1978e3a9f44cSAtari911                foreach ($events as $dateKey => $dayEvents) {
1979e3a9f44cSAtari911                    if (!isset($allEvents[$dateKey])) {
1980e3a9f44cSAtari911                        $allEvents[$dateKey] = array();
1981e3a9f44cSAtari911                    }
1982e3a9f44cSAtari911                    foreach ($dayEvents as $event) {
1983e3a9f44cSAtari911                        $event['_namespace'] = $namespace;
1984e3a9f44cSAtari911                        $allEvents[$dateKey][] = $event;
1985e3a9f44cSAtari911                    }
1986e3a9f44cSAtari911                }
1987e3a9f44cSAtari911
1988e3a9f44cSAtari911                // Recurse into subdirectories
1989e3a9f44cSAtari911                $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents);
1990e3a9f44cSAtari911            }
1991e3a9f44cSAtari911        }
1992e3a9f44cSAtari911    }
19931d05cddcSAtari911
19941d05cddcSAtari911    private function getAllNamespaces() {
19951d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
19961d05cddcSAtari911        $namespaces = [];
19971d05cddcSAtari911
19981d05cddcSAtari911        // Scan for namespaces that have calendar data
19991d05cddcSAtari911        $this->scanForCalendarNamespaces($dataDir, '', $namespaces);
20001d05cddcSAtari911
20011d05cddcSAtari911        // Sort alphabetically
20021d05cddcSAtari911        sort($namespaces);
20031d05cddcSAtari911
20041d05cddcSAtari911        return $namespaces;
20051d05cddcSAtari911    }
20061d05cddcSAtari911
20071d05cddcSAtari911    private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) {
20081d05cddcSAtari911        if (!is_dir($dir)) return;
20091d05cddcSAtari911
20101d05cddcSAtari911        $items = scandir($dir);
20111d05cddcSAtari911        foreach ($items as $item) {
20121d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
20131d05cddcSAtari911
20141d05cddcSAtari911            $path = $dir . $item;
20151d05cddcSAtari911            if (is_dir($path)) {
20161d05cddcSAtari911                // Check if this directory has a calendar subdirectory with data
20171d05cddcSAtari911                $calendarDir = $path . '/calendar/';
20181d05cddcSAtari911                if (is_dir($calendarDir)) {
20191d05cddcSAtari911                    // Check if there are any JSON files in the calendar directory
20201d05cddcSAtari911                    $jsonFiles = glob($calendarDir . '*.json');
20211d05cddcSAtari911                    if (!empty($jsonFiles)) {
20221d05cddcSAtari911                        // This namespace has calendar data
20231d05cddcSAtari911                        $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
20241d05cddcSAtari911                        $namespaces[] = $namespace;
20251d05cddcSAtari911                    }
20261d05cddcSAtari911                }
20271d05cddcSAtari911
20281d05cddcSAtari911                // Recurse into subdirectories
20291d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
20301d05cddcSAtari911                $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces);
20311d05cddcSAtari911            }
20321d05cddcSAtari911        }
20331d05cddcSAtari911    }
20341d05cddcSAtari911
20351d05cddcSAtari911    /**
20361d05cddcSAtari911     * Render new sidebar widget - Week at a glance itinerary (200px wide)
20371d05cddcSAtari911     */
20381d05cddcSAtari911    private function renderSidebarWidget($events, $namespace, $calId) {
20391d05cddcSAtari911        if (empty($events)) {
20401d05cddcSAtari911            return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">No events this week</div>';
20411d05cddcSAtari911        }
20421d05cddcSAtari911
20431d05cddcSAtari911        // Get important namespaces from config
20441d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
20451d05cddcSAtari911        $importantNsList = ['important']; // default
20461d05cddcSAtari911        if (file_exists($configFile)) {
20471d05cddcSAtari911            $config = include $configFile;
20481d05cddcSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
20491d05cddcSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
20501d05cddcSAtari911            }
20511d05cddcSAtari911        }
20521d05cddcSAtari911
20531d05cddcSAtari911        // Calculate date ranges
20541d05cddcSAtari911        $todayStr = date('Y-m-d');
20551d05cddcSAtari911        $tomorrowStr = date('Y-m-d', strtotime('+1 day'));
2056*9ccd446eSAtari911
2057*9ccd446eSAtari911        // Get week start preference and calculate week range
2058*9ccd446eSAtari911        $weekStartDay = $this->getWeekStartDay();
2059*9ccd446eSAtari911
2060*9ccd446eSAtari911        if ($weekStartDay === 'monday') {
2061*9ccd446eSAtari911            // Monday start
20621d05cddcSAtari911            $weekStart = date('Y-m-d', strtotime('monday this week'));
20631d05cddcSAtari911            $weekEnd = date('Y-m-d', strtotime('sunday this week'));
2064*9ccd446eSAtari911        } else {
2065*9ccd446eSAtari911            // Sunday start (default - US/Canada standard)
2066*9ccd446eSAtari911            $today = date('w'); // 0 (Sun) to 6 (Sat)
2067*9ccd446eSAtari911            if ($today == 0) {
2068*9ccd446eSAtari911                // Today is Sunday
2069*9ccd446eSAtari911                $weekStart = date('Y-m-d');
2070*9ccd446eSAtari911            } else {
2071*9ccd446eSAtari911                // Monday-Saturday: go back to last Sunday
2072*9ccd446eSAtari911                $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
2073*9ccd446eSAtari911            }
2074*9ccd446eSAtari911            $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
2075*9ccd446eSAtari911        }
20761d05cddcSAtari911
20771d05cddcSAtari911        // Group events by category
20781d05cddcSAtari911        $todayEvents = [];
20791d05cddcSAtari911        $tomorrowEvents = [];
20801d05cddcSAtari911        $importantEvents = [];
20811d05cddcSAtari911        $weekEvents = []; // For week grid
20821d05cddcSAtari911
20831d05cddcSAtari911        // Process all events
20841d05cddcSAtari911        foreach ($events as $dateKey => $dayEvents) {
2085*9ccd446eSAtari911            // Detect conflicts for events on this day
2086*9ccd446eSAtari911            $eventsWithConflicts = $this->detectTimeConflicts($dayEvents);
20871d05cddcSAtari911
2088*9ccd446eSAtari911            foreach ($eventsWithConflicts as $event) {
2089*9ccd446eSAtari911                // Always categorize Today and Tomorrow regardless of week boundaries
2090*9ccd446eSAtari911                if ($dateKey === $todayStr) {
2091*9ccd446eSAtari911                    $todayEvents[] = array_merge($event, ['date' => $dateKey]);
2092*9ccd446eSAtari911                }
2093*9ccd446eSAtari911                if ($dateKey === $tomorrowStr) {
2094*9ccd446eSAtari911                    $tomorrowEvents[] = array_merge($event, ['date' => $dateKey]);
2095*9ccd446eSAtari911                }
2096*9ccd446eSAtari911
2097*9ccd446eSAtari911                // Process week grid events (only for current week)
20981d05cddcSAtari911                if ($dateKey >= $weekStart && $dateKey <= $weekEnd) {
2099*9ccd446eSAtari911                    // Initialize week grid day if not exists
21001d05cddcSAtari911                    if (!isset($weekEvents[$dateKey])) {
21011d05cddcSAtari911                        $weekEvents[$dateKey] = [];
21021d05cddcSAtari911                    }
21031d05cddcSAtari911
21041d05cddcSAtari911                    // Pre-render DokuWiki syntax to HTML for JavaScript display
21051d05cddcSAtari911                    $eventWithHtml = $event;
21061d05cddcSAtari911                    if (isset($event['title'])) {
21071d05cddcSAtari911                        $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']);
21081d05cddcSAtari911                    }
21091d05cddcSAtari911                    if (isset($event['description'])) {
21101d05cddcSAtari911                        $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']);
21111d05cddcSAtari911                    }
21121d05cddcSAtari911                    $weekEvents[$dateKey][] = $eventWithHtml;
21131d05cddcSAtari911                }
21141d05cddcSAtari911
21151d05cddcSAtari911                // Check if this is an important namespace
21161d05cddcSAtari911                $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
21171d05cddcSAtari911                $isImportant = false;
21181d05cddcSAtari911                foreach ($importantNsList as $impNs) {
21191d05cddcSAtari911                    if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
21201d05cddcSAtari911                        $isImportant = true;
21211d05cddcSAtari911                        break;
21221d05cddcSAtari911                    }
21231d05cddcSAtari911                }
21241d05cddcSAtari911
2125*9ccd446eSAtari911                // Important events: show from today through next 2 weeks
2126*9ccd446eSAtari911                if ($isImportant && $dateKey >= $todayStr) {
21271d05cddcSAtari911                    $importantEvents[] = array_merge($event, ['date' => $dateKey]);
21281d05cddcSAtari911                }
21291d05cddcSAtari911            }
21301d05cddcSAtari911        }
2131*9ccd446eSAtari911
2132*9ccd446eSAtari911        // Sort Important Events by date (earliest first)
2133*9ccd446eSAtari911        usort($importantEvents, function($a, $b) {
2134*9ccd446eSAtari911            $dateA = isset($a['date']) ? $a['date'] : '';
2135*9ccd446eSAtari911            $dateB = isset($b['date']) ? $b['date'] : '';
2136*9ccd446eSAtari911
2137*9ccd446eSAtari911            // Compare dates
2138*9ccd446eSAtari911            if ($dateA === $dateB) {
2139*9ccd446eSAtari911                // Same date - sort by time
2140*9ccd446eSAtari911                $timeA = isset($a['time']) ? $a['time'] : '';
2141*9ccd446eSAtari911                $timeB = isset($b['time']) ? $b['time'] : '';
2142*9ccd446eSAtari911
2143*9ccd446eSAtari911                if (empty($timeA) && !empty($timeB)) return 1;  // All-day events last
2144*9ccd446eSAtari911                if (!empty($timeA) && empty($timeB)) return -1;
2145*9ccd446eSAtari911                if (empty($timeA) && empty($timeB)) return 0;
2146*9ccd446eSAtari911
2147*9ccd446eSAtari911                // Both have times
2148*9ccd446eSAtari911                $aMinutes = $this->timeToMinutes($timeA);
2149*9ccd446eSAtari911                $bMinutes = $this->timeToMinutes($timeB);
2150*9ccd446eSAtari911                return $aMinutes - $bMinutes;
21511d05cddcSAtari911            }
21521d05cddcSAtari911
2153*9ccd446eSAtari911            return strcmp($dateA, $dateB);
2154*9ccd446eSAtari911        });
2155*9ccd446eSAtari911
2156*9ccd446eSAtari911        // Get theme and apply appropriate CSS
2157*9ccd446eSAtari911        $theme = $this->getSidebarTheme();
2158*9ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
2159*9ccd446eSAtari911        $themeClass = 'sidebar-' . $theme;
2160*9ccd446eSAtari911
2161*9ccd446eSAtari911        // Start building HTML - Dynamic width with default font (overflow:visible for tooltips)
2162*9ccd446eSAtari911        $html = '<div class="sidebar-widget ' . $themeClass . '" id="sidebar-widget-' . $calId . '" style="width:100%; max-width:100%; box-sizing:border-box; font-family:system-ui, sans-serif; background:' . $themeStyles['bg'] . '; border:2px solid ' . $themeStyles['border'] . '; border-radius:4px; overflow:visible; box-shadow:0 0 10px ' . $themeStyles['shadow'] . '; position:relative;">';
2163*9ccd446eSAtari911
2164*9ccd446eSAtari911        // Inject CSS variables so the event dialog (shared component) picks up the theme
2165*9ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
2166*9ccd446eSAtari911        $html .= '<style>
2167*9ccd446eSAtari911        #sidebar-widget-' . $calId . ' {
2168*9ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
2169*9ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
2170*9ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
2171*9ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
2172*9ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
2173*9ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
2174*9ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
2175*9ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
2176*9ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
2177*9ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
2178*9ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
2179*9ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
2180*9ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
2181*9ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
2182*9ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
2183*9ccd446eSAtari911        }
2184*9ccd446eSAtari911        </style>';
2185*9ccd446eSAtari911
2186*9ccd446eSAtari911        // Add sparkle effect for pink theme
2187*9ccd446eSAtari911        if ($theme === 'pink') {
2188*9ccd446eSAtari911            $html .= '<style>
2189*9ccd446eSAtari911            @keyframes sparkle-' . $calId . ' {
2190*9ccd446eSAtari911                0% {
2191*9ccd446eSAtari911                    opacity: 0;
2192*9ccd446eSAtari911                    transform: translate(0, 0) scale(0) rotate(0deg);
2193*9ccd446eSAtari911                }
2194*9ccd446eSAtari911                50% {
2195*9ccd446eSAtari911                    opacity: 1;
2196*9ccd446eSAtari911                    transform: translate(var(--tx), var(--ty)) scale(1) rotate(180deg);
2197*9ccd446eSAtari911                }
2198*9ccd446eSAtari911                100% {
2199*9ccd446eSAtari911                    opacity: 0;
2200*9ccd446eSAtari911                    transform: translate(calc(var(--tx) * 2), calc(var(--ty) * 2)) scale(0) rotate(360deg);
2201*9ccd446eSAtari911                }
2202*9ccd446eSAtari911            }
2203*9ccd446eSAtari911
2204*9ccd446eSAtari911            @keyframes pulse-glow-' . $calId . ' {
2205*9ccd446eSAtari911                0%, 100% { box-shadow: 0 0 10px rgba(255, 20, 147, 0.4); }
2206*9ccd446eSAtari911                50% { box-shadow: 0 0 25px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4); }
2207*9ccd446eSAtari911            }
2208*9ccd446eSAtari911
2209*9ccd446eSAtari911            @keyframes shimmer-' . $calId . ' {
2210*9ccd446eSAtari911                0% { background-position: -200% center; }
2211*9ccd446eSAtari911                100% { background-position: 200% center; }
2212*9ccd446eSAtari911            }
2213*9ccd446eSAtari911
2214*9ccd446eSAtari911            .sidebar-pink {
2215*9ccd446eSAtari911                animation: pulse-glow-' . $calId . ' 3s ease-in-out infinite;
2216*9ccd446eSAtari911            }
2217*9ccd446eSAtari911
2218*9ccd446eSAtari911            .sidebar-pink:hover {
2219*9ccd446eSAtari911                box-shadow: 0 0 30px rgba(255, 20, 147, 0.9), 0 0 50px rgba(255, 20, 147, 0.5) !important;
2220*9ccd446eSAtari911            }
2221*9ccd446eSAtari911
2222*9ccd446eSAtari911            .sparkle-' . $calId . ' {
2223*9ccd446eSAtari911                position: absolute;
2224*9ccd446eSAtari911                pointer-events: none;
2225*9ccd446eSAtari911                font-size: 20px;
2226*9ccd446eSAtari911                z-index: 1000;
2227*9ccd446eSAtari911                animation: sparkle-' . $calId . ' 1s ease-out forwards;
2228*9ccd446eSAtari911                filter: drop-shadow(0 0 3px rgba(255, 20, 147, 0.8));
2229*9ccd446eSAtari911            }
2230*9ccd446eSAtari911            </style>';
2231*9ccd446eSAtari911
2232*9ccd446eSAtari911            $html .= '<script>
2233*9ccd446eSAtari911            (function() {
2234*9ccd446eSAtari911                const container = document.getElementById("sidebar-widget-' . $calId . '");
2235*9ccd446eSAtari911                const sparkles = ["✨", "��", "��", "⭐", "��", "��", "��", "��", "��", "��"];
2236*9ccd446eSAtari911
2237*9ccd446eSAtari911                function createSparkle(x, y) {
2238*9ccd446eSAtari911                    const sparkle = document.createElement("div");
2239*9ccd446eSAtari911                    sparkle.className = "sparkle-' . $calId . '";
2240*9ccd446eSAtari911                    sparkle.textContent = sparkles[Math.floor(Math.random() * sparkles.length)];
2241*9ccd446eSAtari911                    sparkle.style.left = x + "px";
2242*9ccd446eSAtari911                    sparkle.style.top = y + "px";
2243*9ccd446eSAtari911
2244*9ccd446eSAtari911                    // Random direction
2245*9ccd446eSAtari911                    const angle = Math.random() * Math.PI * 2;
2246*9ccd446eSAtari911                    const distance = 30 + Math.random() * 40;
2247*9ccd446eSAtari911                    sparkle.style.setProperty("--tx", Math.cos(angle) * distance + "px");
2248*9ccd446eSAtari911                    sparkle.style.setProperty("--ty", Math.sin(angle) * distance + "px");
2249*9ccd446eSAtari911
2250*9ccd446eSAtari911                    container.appendChild(sparkle);
2251*9ccd446eSAtari911
2252*9ccd446eSAtari911                    setTimeout(() => sparkle.remove(), 1000);
2253*9ccd446eSAtari911                }
2254*9ccd446eSAtari911
2255*9ccd446eSAtari911                // Click sparkles
2256*9ccd446eSAtari911                container.addEventListener("click", function(e) {
2257*9ccd446eSAtari911                    const rect = container.getBoundingClientRect();
2258*9ccd446eSAtari911                    const x = e.clientX - rect.left;
2259*9ccd446eSAtari911                    const y = e.clientY - rect.top;
2260*9ccd446eSAtari911
2261*9ccd446eSAtari911                    // Create LOTS of sparkles for maximum bling!
2262*9ccd446eSAtari911                    for (let i = 0; i < 8; i++) {
2263*9ccd446eSAtari911                        setTimeout(() => {
2264*9ccd446eSAtari911                            const offsetX = x + (Math.random() - 0.5) * 30;
2265*9ccd446eSAtari911                            const offsetY = y + (Math.random() - 0.5) * 30;
2266*9ccd446eSAtari911                            createSparkle(offsetX, offsetY);
2267*9ccd446eSAtari911                        }, i * 40);
2268*9ccd446eSAtari911                    }
2269*9ccd446eSAtari911                });
2270*9ccd446eSAtari911
2271*9ccd446eSAtari911                // Random auto-sparkles for extra glamour
2272*9ccd446eSAtari911                setInterval(() => {
2273*9ccd446eSAtari911                    const x = Math.random() * container.offsetWidth;
2274*9ccd446eSAtari911                    const y = Math.random() * container.offsetHeight;
2275*9ccd446eSAtari911                    createSparkle(x, y);
2276*9ccd446eSAtari911                }, 3000);
2277*9ccd446eSAtari911            })();
2278*9ccd446eSAtari911            </script>';
2279*9ccd446eSAtari911        }
22801d05cddcSAtari911
22811d05cddcSAtari911        // Sanitize calId for use in JavaScript variable names (remove dashes)
22821d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
22831d05cddcSAtari911
22841d05cddcSAtari911        // CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it
22851d05cddcSAtari911        $html .= '<script>
22861d05cddcSAtari911(function() {
22871d05cddcSAtari911    // Shared state for system stats and tooltips
22881d05cddcSAtari911    const sharedState_' . $jsCalId . ' = {
22891d05cddcSAtari911        latestStats: {
22901d05cddcSAtari911            load: {"1min": 0, "5min": 0, "15min": 0},
22911d05cddcSAtari911            uptime: "",
22921d05cddcSAtari911            memory_details: {},
22931d05cddcSAtari911            top_processes: []
22941d05cddcSAtari911        },
22951d05cddcSAtari911        cpuHistory: [],
22961d05cddcSAtari911        CPU_HISTORY_SIZE: 2
22971d05cddcSAtari911    };
22981d05cddcSAtari911
22991d05cddcSAtari911    // Tooltip functions - MUST be defined before HTML uses them
23001d05cddcSAtari911    window["showTooltip_' . $jsCalId . '"] = function(color) {
23011d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
23021d05cddcSAtari911        if (!tooltip) {
23031d05cddcSAtari911            console.log("Tooltip element not found for color:", color);
23041d05cddcSAtari911            return;
23051d05cddcSAtari911        }
23061d05cddcSAtari911
23071d05cddcSAtari911        const latestStats = sharedState_' . $jsCalId . '.latestStats;
23081d05cddcSAtari911        let content = "";
23091d05cddcSAtari911
23101d05cddcSAtari911        if (color === "green") {
23111d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">CPU Load Average</div>";
23121d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
23131d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
23141d05cddcSAtari911            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
23151d05cddcSAtari911            if (latestStats.uptime) {
23161d05cddcSAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\\">Uptime: " + latestStats.uptime + "</div>";
23171d05cddcSAtari911            }
23181d05cddcSAtari911            tooltip.style.borderColor = "#00cc07";
23191d05cddcSAtari911            tooltip.style.color = "#00cc07";
23201d05cddcSAtari911        } else if (color === "purple") {
23211d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">CPU Load (Short-term)</div>";
23221d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
23231d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
23241d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
23251d05cddcSAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(155,89,182,0.3);\\" class=\\"tooltip-title\\">Top Processes</div>";
23261d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
23271d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
23281d05cddcSAtari911                });
23291d05cddcSAtari911            }
23301d05cddcSAtari911            tooltip.style.borderColor = "#9b59b6";
23311d05cddcSAtari911            tooltip.style.color = "#9b59b6";
23321d05cddcSAtari911        } else if (color === "orange") {
23331d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">Memory Usage</div>";
23341d05cddcSAtari911            if (latestStats.memory_details && latestStats.memory_details.total) {
23351d05cddcSAtari911                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
23361d05cddcSAtari911                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
23371d05cddcSAtari911                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
23381d05cddcSAtari911                if (latestStats.memory_details.cached) {
23391d05cddcSAtari911                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
23401d05cddcSAtari911                }
23411d05cddcSAtari911            } else {
23421d05cddcSAtari911                content += "<div>Loading...</div>";
23431d05cddcSAtari911            }
23441d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
23451d05cddcSAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(255,140,0,0.3);\\" class=\\"tooltip-title\\">Top Processes</div>";
23461d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
23471d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
23481d05cddcSAtari911                });
23491d05cddcSAtari911            }
23501d05cddcSAtari911            tooltip.style.borderColor = "#ff9800";
23511d05cddcSAtari911            tooltip.style.color = "#ff9800";
23521d05cddcSAtari911        }
23531d05cddcSAtari911
23541d05cddcSAtari911        tooltip.innerHTML = content;
23551d05cddcSAtari911        tooltip.style.display = "block";
23561d05cddcSAtari911
23571d05cddcSAtari911        const bar = tooltip.parentElement;
23581d05cddcSAtari911        const barRect = bar.getBoundingClientRect();
23591d05cddcSAtari911        const tooltipRect = tooltip.getBoundingClientRect();
23601d05cddcSAtari911
23611d05cddcSAtari911        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
23621d05cddcSAtari911        const top = barRect.top - tooltipRect.height - 8;
23631d05cddcSAtari911
23641d05cddcSAtari911        tooltip.style.left = left + "px";
23651d05cddcSAtari911        tooltip.style.top = top + "px";
23661d05cddcSAtari911    };
23671d05cddcSAtari911
23681d05cddcSAtari911    window["hideTooltip_' . $jsCalId . '"] = function(color) {
23691d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
23701d05cddcSAtari911        if (tooltip) {
23711d05cddcSAtari911            tooltip.style.display = "none";
23721d05cddcSAtari911        }
23731d05cddcSAtari911    };
23741d05cddcSAtari911
23751d05cddcSAtari911    // Update clock every second
23761d05cddcSAtari911    function updateClock() {
23771d05cddcSAtari911        const now = new Date();
23781d05cddcSAtari911        let hours = now.getHours();
23791d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
23801d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
23811d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
23821d05cddcSAtari911        hours = hours % 12 || 12;
23831d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
23841d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
23851d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
23861d05cddcSAtari911    }
23871d05cddcSAtari911    setInterval(updateClock, 1000);
23881d05cddcSAtari911
23891d05cddcSAtari911    // Weather update function
23901d05cddcSAtari911    function updateWeather() {
23911d05cddcSAtari911        if ("geolocation" in navigator) {
23921d05cddcSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
23931d05cddcSAtari911                const lat = position.coords.latitude;
23941d05cddcSAtari911                const lon = position.coords.longitude;
23951d05cddcSAtari911
23961d05cddcSAtari911                fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true&temperature_unit=fahrenheit`)
23971d05cddcSAtari911                    .then(response => response.json())
23981d05cddcSAtari911                    .then(data => {
23991d05cddcSAtari911                        if (data.current_weather) {
24001d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
24011d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
24021d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
24031d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
24041d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
24051d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
24061d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
24071d05cddcSAtari911                        }
24081d05cddcSAtari911                    })
24091d05cddcSAtari911                    .catch(error => console.log("Weather fetch error:", error));
24101d05cddcSAtari911            }, function(error) {
24111d05cddcSAtari911                // If geolocation fails, use default location (Irvine, CA)
24121d05cddcSAtari911                fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265&current_weather=true&temperature_unit=fahrenheit")
24131d05cddcSAtari911                    .then(response => response.json())
24141d05cddcSAtari911                    .then(data => {
24151d05cddcSAtari911                        if (data.current_weather) {
24161d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
24171d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
24181d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
24191d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
24201d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
24211d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
24221d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
24231d05cddcSAtari911                        }
24241d05cddcSAtari911                    })
24251d05cddcSAtari911                    .catch(err => console.log("Weather error:", err));
24261d05cddcSAtari911            });
24271d05cddcSAtari911        } else {
24281d05cddcSAtari911            // No geolocation, use default (Irvine, CA)
24291d05cddcSAtari911            fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265&current_weather=true&temperature_unit=fahrenheit")
24301d05cddcSAtari911                .then(response => response.json())
24311d05cddcSAtari911                .then(data => {
24321d05cddcSAtari911                    if (data.current_weather) {
24331d05cddcSAtari911                        const temp = Math.round(data.current_weather.temperature);
24341d05cddcSAtari911                        const weatherCode = data.current_weather.weathercode;
24351d05cddcSAtari911                        const icon = getWeatherIcon(weatherCode);
24361d05cddcSAtari911                        const iconEl = document.getElementById("weather-icon-' . $calId . '");
24371d05cddcSAtari911                        const tempEl = document.getElementById("weather-temp-' . $calId . '");
24381d05cddcSAtari911                        if (iconEl) iconEl.textContent = icon;
24391d05cddcSAtari911                        if (tempEl) tempEl.innerHTML = temp + "&deg;";
24401d05cddcSAtari911                    }
24411d05cddcSAtari911                })
24421d05cddcSAtari911                .catch(err => console.log("Weather error:", err));
24431d05cddcSAtari911        }
24441d05cddcSAtari911    }
24451d05cddcSAtari911
24461d05cddcSAtari911    function getWeatherIcon(code) {
24471d05cddcSAtari911        const icons = {
24481d05cddcSAtari911            0: "☀️", 1: "��️", 2: "⛅", 3: "☁️",
24491d05cddcSAtari911            45: "��️", 48: "��️", 51: "��️", 53: "��️", 55: "��️",
24501d05cddcSAtari911            61: "��️", 63: "��️", 65: "⛈️", 71: "��️", 73: "��️",
24511d05cddcSAtari911            75: "❄️", 77: "��️", 80: "��️", 81: "��️", 82: "⛈️",
24521d05cddcSAtari911            85: "��️", 86: "❄️", 95: "⛈️", 96: "⛈️", 99: "⛈️"
24531d05cddcSAtari911        };
24541d05cddcSAtari911        return icons[code] || "��️";
24551d05cddcSAtari911    }
24561d05cddcSAtari911
24571d05cddcSAtari911    // Update weather immediately and every 10 minutes
24581d05cddcSAtari911    updateWeather();
24591d05cddcSAtari911    setInterval(updateWeather, 600000);
24601d05cddcSAtari911
24611d05cddcSAtari911    // Update system stats and tooltips data
24621d05cddcSAtari911    function updateSystemStats() {
24631d05cddcSAtari911        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
24641d05cddcSAtari911            .then(response => response.json())
24651d05cddcSAtari911            .then(data => {
24661d05cddcSAtari911                sharedState_' . $jsCalId . '.latestStats = {
24671d05cddcSAtari911                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
24681d05cddcSAtari911                    uptime: data.uptime || "",
24691d05cddcSAtari911                    memory_details: data.memory_details || {},
24701d05cddcSAtari911                    top_processes: data.top_processes || []
24711d05cddcSAtari911                };
24721d05cddcSAtari911
24731d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
24741d05cddcSAtari911                if (greenBar) {
24751d05cddcSAtari911                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
24761d05cddcSAtari911                }
24771d05cddcSAtari911
24781d05cddcSAtari911                sharedState_' . $jsCalId . '.cpuHistory.push(data.cpu);
24791d05cddcSAtari911                if (sharedState_' . $jsCalId . '.cpuHistory.length > sharedState_' . $jsCalId . '.CPU_HISTORY_SIZE) {
24801d05cddcSAtari911                    sharedState_' . $jsCalId . '.cpuHistory.shift();
24811d05cddcSAtari911                }
24821d05cddcSAtari911
24831d05cddcSAtari911                const cpuAverage = sharedState_' . $jsCalId . '.cpuHistory.reduce((sum, val) => sum + val, 0) / sharedState_' . $jsCalId . '.cpuHistory.length;
24841d05cddcSAtari911
24851d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
24861d05cddcSAtari911                if (cpuBar) {
24871d05cddcSAtari911                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
24881d05cddcSAtari911                }
24891d05cddcSAtari911
24901d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
24911d05cddcSAtari911                if (memBar) {
24921d05cddcSAtari911                    memBar.style.width = Math.min(100, data.memory) + "%";
24931d05cddcSAtari911                }
24941d05cddcSAtari911            })
24951d05cddcSAtari911            .catch(error => {
24961d05cddcSAtari911                console.log("System stats error:", error);
24971d05cddcSAtari911            });
24981d05cddcSAtari911    }
24991d05cddcSAtari911
25001d05cddcSAtari911    updateSystemStats();
25011d05cddcSAtari911    setInterval(updateSystemStats, 2000);
25021d05cddcSAtari911})();
25031d05cddcSAtari911</script>';
25041d05cddcSAtari911
25051d05cddcSAtari911        // NOW add the header HTML (after JavaScript is defined)
25061d05cddcSAtari911        $todayDate = new DateTime();
25071d05cddcSAtari911        $displayDate = $todayDate->format('D, M j, Y');
25081d05cddcSAtari911        $currentTime = $todayDate->format('g:i:s A');
25091d05cddcSAtari911
2510*9ccd446eSAtari911        $html .= '<div class="eventlist-today-header" style="background:' . $themeStyles['header_bg'] . '; border:2px solid ' . $themeStyles['header_border'] . '; box-shadow:' . $themeStyles['header_shadow'] . ';">';
2511*9ccd446eSAtari911        $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '" style="color:' . $themeStyles['text_bright'] . ';">' . $currentTime . '</span>';
25121d05cddcSAtari911        $html .= '<div class="eventlist-bottom-info">';
2513*9ccd446eSAtari911        $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '" style="color:' . $themeStyles['text_primary'] . ';">--°</span></span>';
2514*9ccd446eSAtari911        $html .= '<span class="eventlist-today-date" style="color:' . $themeStyles['text_dim'] . ';">' . $displayDate . '</span>';
25151d05cddcSAtari911        $html .= '</div>';
25161d05cddcSAtari911
25171d05cddcSAtari911        // Three CPU/Memory bars (all update live)
25181d05cddcSAtari911        $html .= '<div class="eventlist-stats-container">';
25191d05cddcSAtari911
25201d05cddcSAtari911        // 5-minute load average (green, updates every 2 seconds)
25211d05cddcSAtari911        $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $jsCalId . '(\'green\')" onmouseout="hideTooltip_' . $jsCalId . '(\'green\')">';
25221d05cddcSAtari911        $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>';
25231d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
25241d05cddcSAtari911        $html .= '</div>';
25251d05cddcSAtari911
25261d05cddcSAtari911        // Real-time CPU (purple, updates with 5-sec average)
25271d05cddcSAtari911        $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'purple\')" onmouseout="hideTooltip_' . $jsCalId . '(\'purple\')">';
25281d05cddcSAtari911        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>';
25291d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
25301d05cddcSAtari911        $html .= '</div>';
25311d05cddcSAtari911
25321d05cddcSAtari911        // Real-time Memory (orange, updates)
25331d05cddcSAtari911        $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'orange\')" onmouseout="hideTooltip_' . $jsCalId . '(\'orange\')">';
25341d05cddcSAtari911        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>';
25351d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
25361d05cddcSAtari911        $html .= '</div>';
25371d05cddcSAtari911
25381d05cddcSAtari911        $html .= '</div>';
25391d05cddcSAtari911        $html .= '</div>';
25401d05cddcSAtari911
2541231d0edbSAtari911        // Get today's date for default event date
2542231d0edbSAtari911        $todayStr = date('Y-m-d');
2543231d0edbSAtari911
2544*9ccd446eSAtari911        // Thin "Add Event" bar between header and week grid - theme-aware colors
2545*9ccd446eSAtari911        $addBtnBg = $theme === 'matrix' ? '#006400' :
2546*9ccd446eSAtari911                   ($theme === 'purple' ? '#7d3c98' :
2547*9ccd446eSAtari911                   ($theme === 'pink' ? '#b8156f' :
2548*9ccd446eSAtari911                   ($theme === 'wiki' ? $themeStyles['grid_bg'] : '#3498db')));
2549*9ccd446eSAtari911        $addBtnHover = $theme === 'matrix' ? '#004d00' :
2550*9ccd446eSAtari911                      ($theme === 'purple' ? '#5b2c6f' :
2551*9ccd446eSAtari911                      ($theme === 'pink' ? '#8b0f54' :
2552*9ccd446eSAtari911                      ($theme === 'wiki' ? $themeStyles['cell_today_bg'] : '#2980b9')));
2553*9ccd446eSAtari911        $addBtnTextColor = $theme === 'professional' ? '#ffffff' :
2554*9ccd446eSAtari911                          ($theme === 'wiki' ? $themeStyles['text_primary'] :
2555*9ccd446eSAtari911                          ($theme === 'pink' ? '#000000' : $themeStyles['text_bright']));
2556*9ccd446eSAtari911                          ($theme === 'pink' ? '#000000' : $themeStyles['text_bright']);
2557*9ccd446eSAtari911        $addBtnShadow = $theme === 'matrix' ? '0 0 8px rgba(0, 100, 0, 0.4)' :
2558*9ccd446eSAtari911                       ($theme === 'purple' ? '0 0 8px rgba(155, 89, 182, 0.4)' :
2559*9ccd446eSAtari911                       ($theme === 'pink' ? '0 0 10px rgba(255, 20, 147, 0.5)' : '0 2px 4px rgba(0,0,0,0.2)'));
2560*9ccd446eSAtari911        $addBtnHoverShadow = $theme === 'matrix' ? '0 0 12px rgba(0, 100, 0, 0.6)' :
2561*9ccd446eSAtari911                            ($theme === 'purple' ? '0 0 12px rgba(155, 89, 182, 0.6)' :
2562*9ccd446eSAtari911                            ($theme === 'pink' ? '0 0 14px rgba(255, 20, 147, 0.7)' : '0 3px 6px rgba(0,0,0,0.3)'));
2563*9ccd446eSAtari911
2564*9ccd446eSAtari911        $html .= '<div style="background:' . $addBtnBg . '; padding:0; margin:0; height:12px; line-height:10px; text-align:center; cursor:pointer; border-top:1px solid rgba(0, 0, 0, 0.1); border-bottom:1px solid rgba(0, 0, 0, 0.1); box-shadow:' . $addBtnShadow . '; transition:all 0.2s;" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\', \'' . $todayStr . '\');" onmouseover="this.style.background=\'' . $addBtnHover . '\'; this.style.boxShadow=\'' . $addBtnHoverShadow . '\';" onmouseout="this.style.background=\'' . $addBtnBg . '\'; this.style.boxShadow=\'' . $addBtnShadow . '\';">';
2565*9ccd446eSAtari911        $addBtnTextShadow = ($theme === 'pink') ? '0 0 3px ' . $addBtnTextColor : 'none';
2566*9ccd446eSAtari911        $html .= '<span style="color:' . $addBtnTextColor . '; font-size:8px; font-weight:700; letter-spacing:0.4px; font-family:system-ui, sans-serif; text-shadow:' . $addBtnTextShadow . '; position:relative; top:-1px;">+ ADD EVENT</span>';
25671d05cddcSAtari911        $html .= '</div>';
25681d05cddcSAtari911
25691d05cddcSAtari911        // Week grid (7 cells)
2570*9ccd446eSAtari911        $html .= $this->renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme);
25711d05cddcSAtari911
2572*9ccd446eSAtari911        // Section colors - different shades for pink theme, template colors for wiki
2573*9ccd446eSAtari911        if ($theme === 'wiki') {
2574*9ccd446eSAtari911            $todayColor = '#e67e22';      // Warm orange - stands out on light bg
2575*9ccd446eSAtari911            $tomorrowColor = '#27ae60';    // Green - distinct from orange
2576*9ccd446eSAtari911            $importantColor = '#8e44ad';   // Purple - distinct from both
2577*9ccd446eSAtari911        } else {
2578*9ccd446eSAtari911            $todayColor = $theme === 'pink' ? '#ff1493' : '#ff9800';      // Hot pink vs orange
2579*9ccd446eSAtari911            $tomorrowColor = $theme === 'pink' ? '#ff69b4' : '#4caf50';   // Pink vs green
2580*9ccd446eSAtari911            $importantColor = $theme === 'pink' ? '#ff85c1' : '#9b59b6';  // Light pink vs purple
2581*9ccd446eSAtari911        }
2582*9ccd446eSAtari911
2583*9ccd446eSAtari911        // Today section
25841d05cddcSAtari911        if (!empty($todayEvents)) {
2585*9ccd446eSAtari911            $html .= $this->renderSidebarSection('Today', $todayEvents, $todayColor, $calId, $themeStyles, $theme);
25861d05cddcSAtari911        }
25871d05cddcSAtari911
2588*9ccd446eSAtari911        // Tomorrow section
25891d05cddcSAtari911        if (!empty($tomorrowEvents)) {
2590*9ccd446eSAtari911            $html .= $this->renderSidebarSection('Tomorrow', $tomorrowEvents, $tomorrowColor, $calId, $themeStyles, $theme);
25911d05cddcSAtari911        }
25921d05cddcSAtari911
2593*9ccd446eSAtari911        // Important events section
25941d05cddcSAtari911        if (!empty($importantEvents)) {
2595*9ccd446eSAtari911            $html .= $this->renderSidebarSection('Important Events', $importantEvents, $importantColor, $calId, $themeStyles, $theme);
25961d05cddcSAtari911        }
25971d05cddcSAtari911
25981d05cddcSAtari911        $html .= '</div>';
25991d05cddcSAtari911
2600231d0edbSAtari911        // Add event dialog for sidebar widget
2601231d0edbSAtari911        $html .= $this->renderEventDialog($calId, $namespace);
2602231d0edbSAtari911
2603*9ccd446eSAtari911        // Add JavaScript for positioning data-tooltip elements
2604*9ccd446eSAtari911        $html .= '<script>
2605*9ccd446eSAtari911        // Position data-tooltip elements to prevent cutoff (up and to the LEFT)
2606*9ccd446eSAtari911        document.addEventListener("DOMContentLoaded", function() {
2607*9ccd446eSAtari911            const tooltipElements = document.querySelectorAll("[data-tooltip]");
2608*9ccd446eSAtari911            const isPinkTheme = document.querySelector(".sidebar-pink") !== null;
2609*9ccd446eSAtari911
2610*9ccd446eSAtari911            tooltipElements.forEach(function(element) {
2611*9ccd446eSAtari911                element.addEventListener("mouseenter", function() {
2612*9ccd446eSAtari911                    const rect = element.getBoundingClientRect();
2613*9ccd446eSAtari911                    const style = window.getComputedStyle(element, ":before");
2614*9ccd446eSAtari911
2615*9ccd446eSAtari911                    // Position above the element, aligned to LEFT (not right)
2616*9ccd446eSAtari911                    element.style.setProperty("--tooltip-left", (rect.left - 150) + "px");
2617*9ccd446eSAtari911                    element.style.setProperty("--tooltip-top", (rect.top - 30) + "px");
2618*9ccd446eSAtari911
2619*9ccd446eSAtari911                    // Pink theme: position heart to the right of tooltip
2620*9ccd446eSAtari911                    if (isPinkTheme) {
2621*9ccd446eSAtari911                        element.style.setProperty("--heart-left", (rect.left - 150 + 210) + "px");
2622*9ccd446eSAtari911                        element.style.setProperty("--heart-top", (rect.top - 30) + "px");
2623*9ccd446eSAtari911                    }
2624*9ccd446eSAtari911                });
2625*9ccd446eSAtari911            });
2626*9ccd446eSAtari911        });
2627*9ccd446eSAtari911
2628*9ccd446eSAtari911        // Apply custom properties to position tooltips
2629*9ccd446eSAtari911        const style = document.createElement("style");
2630*9ccd446eSAtari911        style.textContent = `
2631*9ccd446eSAtari911            [data-tooltip]:hover:before {
2632*9ccd446eSAtari911                left: var(--tooltip-left, 0) !important;
2633*9ccd446eSAtari911                top: var(--tooltip-top, 0) !important;
2634*9ccd446eSAtari911            }
2635*9ccd446eSAtari911            .sidebar-pink [data-tooltip]:hover:after {
2636*9ccd446eSAtari911                left: var(--heart-left, 0) !important;
2637*9ccd446eSAtari911                top: var(--heart-top, 0) !important;
2638*9ccd446eSAtari911            }
2639*9ccd446eSAtari911        `;
2640*9ccd446eSAtari911        document.head.appendChild(style);
2641*9ccd446eSAtari911        </script>';
2642*9ccd446eSAtari911
26431d05cddcSAtari911        return $html;
26441d05cddcSAtari911    }
26451d05cddcSAtari911
26461d05cddcSAtari911    /**
2647*9ccd446eSAtari911     * Render compact week grid (7 cells with event bars) - Theme-aware
26481d05cddcSAtari911     */
2649*9ccd446eSAtari911    private function renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme) {
26501d05cddcSAtari911        // Generate unique ID for this calendar instance - sanitize for JavaScript
26511d05cddcSAtari911        $calId = 'cal_' . substr(md5($weekStart . microtime()), 0, 8);
26521d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);  // Sanitize for JS variable names
26531d05cddcSAtari911
2654*9ccd446eSAtari911        $html = '<div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:' . $themeStyles['grid_bg'] . '; border-bottom:2px solid ' . $themeStyles['grid_border'] . ';">';
26551d05cddcSAtari911
2656*9ccd446eSAtari911        // Day names depend on week start setting
2657*9ccd446eSAtari911        $weekStartDay = $this->getWeekStartDay();
2658*9ccd446eSAtari911        if ($weekStartDay === 'monday') {
2659*9ccd446eSAtari911            $dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];  // Monday to Sunday
2660*9ccd446eSAtari911        } else {
2661*9ccd446eSAtari911            $dayNames = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];  // Sunday to Saturday
2662*9ccd446eSAtari911        }
26631d05cddcSAtari911        $today = date('Y-m-d');
26641d05cddcSAtari911
26651d05cddcSAtari911        for ($i = 0; $i < 7; $i++) {
26661d05cddcSAtari911            $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days'));
26671d05cddcSAtari911            $dayNum = date('j', strtotime($date));
26681d05cddcSAtari911            $isToday = $date === $today;
26691d05cddcSAtari911
26701d05cddcSAtari911            $events = isset($weekEvents[$date]) ? $weekEvents[$date] : [];
26711d05cddcSAtari911            $eventCount = count($events);
26721d05cddcSAtari911
2673*9ccd446eSAtari911            $bgColor = $isToday ? $themeStyles['cell_today_bg'] : $themeStyles['cell_bg'];
2674*9ccd446eSAtari911            $textColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
26751d05cddcSAtari911            $fontWeight = $isToday ? '700' : '500';
2676*9ccd446eSAtari911
2677*9ccd446eSAtari911            // Theme-aware text shadow
2678*9ccd446eSAtari911            if ($theme === 'pink') {
2679*9ccd446eSAtari911                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
2680*9ccd446eSAtari911                $textShadow = $isToday ? 'text-shadow:0 0 6px ' . $glowColor . ';' : 'text-shadow:0 0 4px ' . $glowColor . ';';
2681*9ccd446eSAtari911            } else {
2682*9ccd446eSAtari911                $textShadow = '';  // No glow for other themes
2683*9ccd446eSAtari911            }
2684*9ccd446eSAtari911
2685*9ccd446eSAtari911            // Border color based on theme
2686*9ccd446eSAtari911            $borderColor = $themeStyles['grid_border'];
26871d05cddcSAtari911
26881d05cddcSAtari911            $hasEvents = $eventCount > 0;
26891d05cddcSAtari911            $clickableStyle = $hasEvents ? 'cursor:pointer;' : '';
26901d05cddcSAtari911            $clickHandler = $hasEvents ? ' onclick="showDayEvents_' . $jsCalId . '(\'' . $date . '\')"' : '';
26911d05cddcSAtari911
2692*9ccd446eSAtari911            $html .= '<div style="background:' . $bgColor . '; padding:4px 2px; text-align:center; min-height:45px; position:relative; border:1px solid ' . $borderColor . ' !important; ' . $clickableStyle . '" ' . $clickHandler . '>';
26931d05cddcSAtari911
2694*9ccd446eSAtari911            // Day letter - theme color
2695*9ccd446eSAtari911            $dayLetterColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary'];
2696*9ccd446eSAtari911            $html .= '<div style="font-size:9px; color:' . $dayLetterColor . '; font-weight:500; font-family:system-ui, sans-serif;">' . $dayNames[$i] . '</div>';
26971d05cddcSAtari911
26981d05cddcSAtari911            // Day number
26991d05cddcSAtari911            $html .= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:2px 0; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNum . '</div>';
27001d05cddcSAtari911
2701*9ccd446eSAtari911            // Event bars (max 4 visible) with theme-aware glow
27021d05cddcSAtari911            if ($eventCount > 0) {
2703*9ccd446eSAtari911                $showCount = min($eventCount, 4);
27041d05cddcSAtari911                for ($j = 0; $j < $showCount; $j++) {
27051d05cddcSAtari911                    $event = $events[$j];
2706*9ccd446eSAtari911                    $color = isset($event['color']) ? $event['color'] : $themeStyles['text_primary'];
2707*9ccd446eSAtari911                    $barShadow = $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . htmlspecialchars($color);
2708*9ccd446eSAtari911                    $html .= '<div style="height:2px; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:' . $barShadow . ';"></div>';
27091d05cddcSAtari911                }
27101d05cddcSAtari911
2711*9ccd446eSAtari911                // Show "+N more" if more than 4 - theme color
2712*9ccd446eSAtari911                if ($eventCount > 4) {
2713*9ccd446eSAtari911                    $moreTextColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary'];
2714*9ccd446eSAtari911                    $html .= '<div style="font-size:7px; color:' . $moreTextColor . '; margin-top:1px; font-family:system-ui, sans-serif;">+' . ($eventCount - 4) . '</div>';
27151d05cddcSAtari911                }
27161d05cddcSAtari911            }
27171d05cddcSAtari911
27181d05cddcSAtari911            $html .= '</div>';
27191d05cddcSAtari911        }
27201d05cddcSAtari911
27211d05cddcSAtari911        $html .= '</div>';
27221d05cddcSAtari911
2723*9ccd446eSAtari911        // Add container for selected day events display (with unique ID) - theme-aware
2724*9ccd446eSAtari911        $panelBorderColor = $theme === 'matrix' ? '#00cc07' :
2725*9ccd446eSAtari911                           ($theme === 'purple' ? '#9b59b6' :
2726*9ccd446eSAtari911                           ($theme === 'pink' ? '#ff1493' :
2727*9ccd446eSAtari911                           ($theme === 'wiki' ? $themeStyles['border'] : '#3498db')));
2728*9ccd446eSAtari911        $panelHeaderBg = $theme === 'matrix' ? '#00cc07' :
2729*9ccd446eSAtari911                        ($theme === 'purple' ? '#9b59b6' :
2730*9ccd446eSAtari911                        ($theme === 'pink' ? '#ff1493' :
2731*9ccd446eSAtari911                        ($theme === 'wiki' ? $themeStyles['border'] : '#3498db')));
2732*9ccd446eSAtari911        $panelShadow = $theme === 'matrix' ? '0 0 5px rgba(0, 204, 7, 0.2)' :
2733*9ccd446eSAtari911                      ($theme === 'purple' ? '0 0 5px rgba(155, 89, 182, 0.2)' :
2734*9ccd446eSAtari911                      ($theme === 'pink' ? '0 0 8px rgba(255, 20, 147, 0.4)' :
2735*9ccd446eSAtari911                      '0 1px 3px rgba(0, 0, 0, 0.1)'));
2736*9ccd446eSAtari911        $panelContentBg = $theme === 'professional' ? 'rgba(255, 255, 255, 0.95)' :
2737*9ccd446eSAtari911                         ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.5)');
2738*9ccd446eSAtari911        $panelHeaderShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $panelHeaderBg;
2739*9ccd446eSAtari911
2740*9ccd446eSAtari911        // Header text color - white for colored headers, dark for light headers
2741*9ccd446eSAtari911        $panelHeaderColor = ($theme === 'wiki' || $theme === 'professional') ? '#fff' : '#000';
2742*9ccd446eSAtari911
2743*9ccd446eSAtari911        $html .= '<div id="selected-day-events-' . $calId . '" style="display:none; margin:8px 4px; border-left:3px solid ' . $panelBorderColor . '; box-shadow:' . $panelShadow . ';">';
2744*9ccd446eSAtari911        $html .= '<div style="background:' . $panelHeaderBg . '; color:' . $panelHeaderColor . '; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:' . $panelHeaderShadow . '; display:flex; justify-content:space-between; align-items:center;">';
27451d05cddcSAtari911        $html .= '<span id="selected-day-title-' . $calId . '"></span>';
2746*9ccd446eSAtari911        $html .= '<span onclick="document.getElementById(\'selected-day-events-' . $calId . '\').style.display=\'none\';" style="cursor:pointer; font-size:12px; padding:0 4px; font-weight:700; color:' . $panelHeaderColor . ';">✕</span>';
27471d05cddcSAtari911        $html .= '</div>';
2748*9ccd446eSAtari911        $html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:' . $panelContentBg . ';"></div>';
27491d05cddcSAtari911        $html .= '</div>';
27501d05cddcSAtari911
27511d05cddcSAtari911        // Add JavaScript for day selection with event data
27521d05cddcSAtari911        $html .= '<script>';
27531d05cddcSAtari911        // Sanitize calId for JavaScript variable names
27541d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
27551d05cddcSAtari911        $html .= 'window.weekEventsData_' . $jsCalId . ' = ' . json_encode($weekEvents) . ';';
2756*9ccd446eSAtari911
2757*9ccd446eSAtari911        // Pass theme colors to JavaScript
2758*9ccd446eSAtari911        $jsThemeColors = json_encode([
2759*9ccd446eSAtari911            'text_primary' => $themeStyles['text_primary'],
2760*9ccd446eSAtari911            'text_bright' => $themeStyles['text_bright'],
2761*9ccd446eSAtari911            'text_dim' => $themeStyles['text_dim'],
2762*9ccd446eSAtari911            'text_shadow' => ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $themeStyles['text_primary'] : '',
2763*9ccd446eSAtari911            'event_bg' => $theme === 'professional' ? 'rgba(255, 255, 255, 0.5)' :
2764*9ccd446eSAtari911                         ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.3)'),
2765*9ccd446eSAtari911            'border_color' => $theme === 'professional' ? 'rgba(0, 0, 0, 0.1)' :
2766*9ccd446eSAtari911                             ($theme === 'purple' ? 'rgba(155, 89, 182, 0.2)' :
2767*9ccd446eSAtari911                             ($theme === 'pink' ? 'rgba(255, 20, 147, 0.3)' :
2768*9ccd446eSAtari911                             ($theme === 'wiki' ? $themeStyles['grid_border'] : 'rgba(0, 204, 7, 0.2)'))),
2769*9ccd446eSAtari911            'bar_shadow' => $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' :
2770*9ccd446eSAtari911                           ($theme === 'wiki' ? '0 1px 2px rgba(0,0,0,0.15)' : '0 0 3px')
2771*9ccd446eSAtari911        ]);
2772*9ccd446eSAtari911        $html .= 'window.themeColors_' . $jsCalId . ' = ' . $jsThemeColors . ';';
27731d05cddcSAtari911        $html .= '
27741d05cddcSAtari911        window.showDayEvents_' . $jsCalId . ' = function(dateKey) {
27751d05cddcSAtari911            const eventsData = window.weekEventsData_' . $jsCalId . ';
27761d05cddcSAtari911            const container = document.getElementById("selected-day-events-' . $calId . '");
27771d05cddcSAtari911            const title = document.getElementById("selected-day-title-' . $calId . '");
27781d05cddcSAtari911            const content = document.getElementById("selected-day-content-' . $calId . '");
27791d05cddcSAtari911
27801d05cddcSAtari911            if (!eventsData[dateKey] || eventsData[dateKey].length === 0) return;
27811d05cddcSAtari911
27821d05cddcSAtari911            // Format date for display
27831d05cddcSAtari911            const dateObj = new Date(dateKey + "T00:00:00");
27841d05cddcSAtari911            const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" });
27851d05cddcSAtari911            const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" });
27861d05cddcSAtari911            title.textContent = dayName + ", " + monthDay;
27871d05cddcSAtari911
27881d05cddcSAtari911            // Clear content
27891d05cddcSAtari911            content.innerHTML = "";
27901d05cddcSAtari911
2791231d0edbSAtari911            // Sort events by time (all-day events first, then timed events chronologically)
27921d05cddcSAtari911            const sortedEvents = [...eventsData[dateKey]].sort((a, b) => {
2793231d0edbSAtari911                // All-day events (no time) go to the beginning
27941d05cddcSAtari911                if (!a.time && !b.time) return 0;
2795231d0edbSAtari911                if (!a.time) return -1;  // a is all-day, comes first
2796231d0edbSAtari911                if (!b.time) return 1;   // b is all-day, comes first
27971d05cddcSAtari911
27981d05cddcSAtari911                // Compare times (format: "HH:MM")
27991d05cddcSAtari911                const timeA = a.time.split(":").map(Number);
28001d05cddcSAtari911                const timeB = b.time.split(":").map(Number);
28011d05cddcSAtari911                const minutesA = timeA[0] * 60 + timeA[1];
28021d05cddcSAtari911                const minutesB = timeB[0] * 60 + timeB[1];
28031d05cddcSAtari911
28041d05cddcSAtari911                return minutesA - minutesB;
28051d05cddcSAtari911            });
28061d05cddcSAtari911
2807*9ccd446eSAtari911            // Build events HTML with single color bar (event color only) - theme-aware
2808*9ccd446eSAtari911            const themeColors = window.themeColors_' . $jsCalId . ';
28091d05cddcSAtari911            sortedEvents.forEach(event => {
2810*9ccd446eSAtari911                const eventColor = event.color || themeColors.text_primary;
28111d05cddcSAtari911
28121d05cddcSAtari911                const eventDiv = document.createElement("div");
2813*9ccd446eSAtari911                eventDiv.style.cssText = "padding:4px 6px; border-bottom:1px solid " + themeColors.border_color + "; font-size:10px; display:flex; align-items:stretch; gap:6px; background:" + themeColors.event_bg + "; min-height:20px;";
28141d05cddcSAtari911
28151d05cddcSAtari911                let eventHTML = "";
28161d05cddcSAtari911
2817*9ccd446eSAtari911                // Event assigned color bar (single bar on left) - theme-aware shadow
2818*9ccd446eSAtari911                const barShadow = themeColors.bar_shadow + (themeColors.bar_shadow.includes("rgba") ? "" : " " + eventColor);
2819*9ccd446eSAtari911                eventHTML += "<div style=\\"width:3px; align-self:stretch; background:" + eventColor + "; border-radius:1px; flex-shrink:0; box-shadow:" + barShadow + ";\\"></div>";
28201d05cddcSAtari911
2821231d0edbSAtari911                // Content wrapper
2822231d0edbSAtari911                eventHTML += "<div style=\\"flex:1; min-width:0; display:flex; justify-content:space-between; align-items:start; gap:4px;\\">";
28231d05cddcSAtari911
2824231d0edbSAtari911                // Left side: event details
28251d05cddcSAtari911                eventHTML += "<div style=\\"flex:1; min-width:0;\\">";
2826*9ccd446eSAtari911                eventHTML += "<div style=\\"font-weight:600; color:" + themeColors.text_primary + "; word-wrap:break-word; font-family:system-ui, sans-serif; " + themeColors.text_shadow + ";\\">";
28271d05cddcSAtari911
28281d05cddcSAtari911                // Time
28291d05cddcSAtari911                if (event.time) {
28301d05cddcSAtari911                    const timeParts = event.time.split(":");
28311d05cddcSAtari911                    let hours = parseInt(timeParts[0]);
28321d05cddcSAtari911                    const minutes = timeParts[1];
28331d05cddcSAtari911                    const ampm = hours >= 12 ? "PM" : "AM";
28341d05cddcSAtari911                    hours = hours % 12 || 12;
2835*9ccd446eSAtari911                    eventHTML += "<span style=\\"color:" + themeColors.text_bright + "; font-weight:500; font-size:9px;\\">" + hours + ":" + minutes + " " + ampm + "</span> ";
28361d05cddcSAtari911                }
28371d05cddcSAtari911
28381d05cddcSAtari911                // Title - use HTML version if available
28391d05cddcSAtari911                const titleHTML = event.title_html || event.title || "Untitled";
28401d05cddcSAtari911                eventHTML += titleHTML;
28411d05cddcSAtari911                eventHTML += "</div>";
28421d05cddcSAtari911
2843*9ccd446eSAtari911                // Description if present - use HTML version - theme-aware color
28441d05cddcSAtari911                if (event.description_html || event.description) {
28451d05cddcSAtari911                    const descHTML = event.description_html || event.description;
2846*9ccd446eSAtari911                    eventHTML += "<div style=\\"font-size:9px; color:" + themeColors.text_dim + "; margin-top:2px;\\">" + descHTML + "</div>";
28471d05cddcSAtari911                }
28481d05cddcSAtari911
2849231d0edbSAtari911                eventHTML += "</div>"; // Close event details
2850231d0edbSAtari911
2851*9ccd446eSAtari911                // Right side: conflict badge with tooltip
2852231d0edbSAtari911                if (event.conflict) {
2853*9ccd446eSAtari911                    let conflictList = [];
2854*9ccd446eSAtari911                    if (event.conflictingWith && event.conflictingWith.length > 0) {
2855*9ccd446eSAtari911                        event.conflictingWith.forEach(conf => {
2856*9ccd446eSAtari911                            const confTime = conf.time + (conf.end_time ? " - " + conf.end_time : "");
2857*9ccd446eSAtari911                            conflictList.push(conf.title + " (" + confTime + ")");
2858*9ccd446eSAtari911                        });
2859*9ccd446eSAtari911                    }
2860*9ccd446eSAtari911                    const conflictData = btoa(unescape(encodeURIComponent(JSON.stringify(conflictList))));
2861*9ccd446eSAtari911                    eventHTML += "<span class=\\"event-conflict-badge\\" style=\\"font-size:10px;\\" data-conflicts=\\"" + conflictData + "\\" onmouseenter=\\"showConflictTooltip(this)\\" onmouseleave=\\"hideConflictTooltip()\\">⚠️ " + (event.conflictingWith ? event.conflictingWith.length : 1) + "</span>";
2862231d0edbSAtari911                }
2863231d0edbSAtari911
2864231d0edbSAtari911                eventHTML += "</div>"; // Close content wrapper
28651d05cddcSAtari911
28661d05cddcSAtari911                eventDiv.innerHTML = eventHTML;
28671d05cddcSAtari911                content.appendChild(eventDiv);
28681d05cddcSAtari911            });
28691d05cddcSAtari911
28701d05cddcSAtari911            container.style.display = "block";
28711d05cddcSAtari911        };
28721d05cddcSAtari911        ';
28731d05cddcSAtari911        $html .= '</script>';
28741d05cddcSAtari911
28751d05cddcSAtari911        return $html;
28761d05cddcSAtari911    }
28771d05cddcSAtari911
28781d05cddcSAtari911    /**
28791d05cddcSAtari911     * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders
28801d05cddcSAtari911     */
2881*9ccd446eSAtari911    private function renderSidebarSection($title, $events, $accentColor, $calId, $themeStyles, $theme) {
28821d05cddcSAtari911        // Keep the original accent colors for borders
28831d05cddcSAtari911        $borderColor = $accentColor;
28841d05cddcSAtari911
28851d05cddcSAtari911        // Show date for Important Events section
28861d05cddcSAtari911        $showDate = ($title === 'Important Events');
28871d05cddcSAtari911
2888*9ccd446eSAtari911        // Sort events differently based on section
2889*9ccd446eSAtari911        if ($title === 'Important Events') {
2890*9ccd446eSAtari911            // Important Events: sort by date first, then by time
2891*9ccd446eSAtari911            usort($events, function($a, $b) {
2892*9ccd446eSAtari911                $aDate = isset($a['date']) ? $a['date'] : '';
2893*9ccd446eSAtari911                $bDate = isset($b['date']) ? $b['date'] : '';
28941d05cddcSAtari911
2895*9ccd446eSAtari911                // Different dates - sort by date
2896*9ccd446eSAtari911                if ($aDate !== $bDate) {
2897*9ccd446eSAtari911                    return strcmp($aDate, $bDate);
2898*9ccd446eSAtari911                }
2899*9ccd446eSAtari911
2900*9ccd446eSAtari911                // Same date - sort by time
2901*9ccd446eSAtari911                $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : '';
2902*9ccd446eSAtari911                $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : '';
2903*9ccd446eSAtari911
2904*9ccd446eSAtari911                // All-day events last within same date
2905*9ccd446eSAtari911                if (empty($aTime) && !empty($bTime)) return 1;
2906*9ccd446eSAtari911                if (!empty($aTime) && empty($bTime)) return -1;
2907*9ccd446eSAtari911                if (empty($aTime) && empty($bTime)) return 0;
2908*9ccd446eSAtari911
2909*9ccd446eSAtari911                // Both have times
2910*9ccd446eSAtari911                $aMinutes = $this->timeToMinutes($aTime);
2911*9ccd446eSAtari911                $bMinutes = $this->timeToMinutes($bTime);
2912*9ccd446eSAtari911                return $aMinutes - $bMinutes;
2913*9ccd446eSAtari911            });
2914*9ccd446eSAtari911        } else {
2915*9ccd446eSAtari911            // Today/Tomorrow: sort by time only (all same date)
2916*9ccd446eSAtari911            usort($events, function($a, $b) {
2917*9ccd446eSAtari911                $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : '';
2918*9ccd446eSAtari911                $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : '';
2919*9ccd446eSAtari911
2920*9ccd446eSAtari911                // All-day events (no time) come first
2921*9ccd446eSAtari911                if (empty($aTime) && !empty($bTime)) return -1;
2922*9ccd446eSAtari911                if (!empty($aTime) && empty($bTime)) return 1;
2923*9ccd446eSAtari911                if (empty($aTime) && empty($bTime)) return 0;
2924*9ccd446eSAtari911
2925*9ccd446eSAtari911                // Both have times - convert to minutes for proper chronological sort
2926*9ccd446eSAtari911                $aMinutes = $this->timeToMinutes($aTime);
2927*9ccd446eSAtari911                $bMinutes = $this->timeToMinutes($bTime);
2928*9ccd446eSAtari911
2929*9ccd446eSAtari911                return $aMinutes - $bMinutes;
2930*9ccd446eSAtari911            });
2931*9ccd446eSAtari911        }
2932*9ccd446eSAtari911
2933*9ccd446eSAtari911        // Theme-aware section shadow
2934*9ccd446eSAtari911        $sectionShadow = $theme === 'matrix' ? '0 0 5px rgba(0, 204, 7, 0.2)' :
2935*9ccd446eSAtari911                        ($theme === 'purple' ? '0 0 5px rgba(155, 89, 182, 0.2)' :
2936*9ccd446eSAtari911                        ($theme === 'pink' ? '0 0 8px rgba(255, 20, 147, 0.4)' :
2937*9ccd446eSAtari911                        '0 1px 3px rgba(0, 0, 0, 0.1)'));
2938*9ccd446eSAtari911
2939*9ccd446eSAtari911        $html = '<div style="border-left:3px solid ' . $borderColor . '; margin:8px 4px; box-shadow:' . $sectionShadow . ';">';
2940*9ccd446eSAtari911
2941*9ccd446eSAtari911        // Section header with accent color background - theme-aware shadow
2942*9ccd446eSAtari911        $headerShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $accentColor;
2943*9ccd446eSAtari911        $headerTextColor = ($theme === 'wiki') ? '#fff' : '#000';
2944*9ccd446eSAtari911        $html .= '<div style="background:' . $accentColor . '; color:' . $headerTextColor . '; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:' . $headerShadow . ';">';
29451d05cddcSAtari911        $html .= htmlspecialchars($title);
29461d05cddcSAtari911        $html .= '</div>';
29471d05cddcSAtari911
2948*9ccd446eSAtari911        // Events - no background (transparent)
2949*9ccd446eSAtari911        $html .= '<div style="padding:4px 0;">';
29501d05cddcSAtari911
29511d05cddcSAtari911        foreach ($events as $event) {
2952*9ccd446eSAtari911            $html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor, $themeStyles, $theme);
29531d05cddcSAtari911        }
29541d05cddcSAtari911
29551d05cddcSAtari911        $html .= '</div>';
29561d05cddcSAtari911        $html .= '</div>';
29571d05cddcSAtari911
29581d05cddcSAtari911        return $html;
29591d05cddcSAtari911    }
29601d05cddcSAtari911
29611d05cddcSAtari911    /**
2962*9ccd446eSAtari911     * Render individual event in sidebar - Theme-aware
29631d05cddcSAtari911     */
2964*9ccd446eSAtari911    private function renderSidebarEvent($event, $calId, $showDate = false, $sectionColor = '#00cc07', $themeStyles = null, $theme = 'matrix') {
29651d05cddcSAtari911        $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
29661d05cddcSAtari911        $time = isset($event['time']) ? $event['time'] : '';
29671d05cddcSAtari911        $endTime = isset($event['endTime']) ? $event['endTime'] : '';
2968*9ccd446eSAtari911        $eventColor = isset($event['color']) ? htmlspecialchars($event['color']) : ($themeStyles ? $themeStyles['text_primary'] : '#00cc07');
29691d05cddcSAtari911        $date = isset($event['date']) ? $event['date'] : '';
29701d05cddcSAtari911        $isTask = isset($event['isTask']) && $event['isTask'];
29711d05cddcSAtari911        $completed = isset($event['completed']) && $event['completed'];
29721d05cddcSAtari911
2973*9ccd446eSAtari911        // Theme-aware colors
2974*9ccd446eSAtari911        $titleColor = $themeStyles ? $themeStyles['text_primary'] : '#00cc07';
2975*9ccd446eSAtari911        $timeColor = $themeStyles ? $themeStyles['text_bright'] : '#00dd00';
2976*9ccd446eSAtari911        $textShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $titleColor . ';' : '';
29771d05cddcSAtari911
2978*9ccd446eSAtari911        // Check for conflicts (using 'conflict' field set by detectTimeConflicts)
2979*9ccd446eSAtari911        $hasConflict = isset($event['conflict']) && $event['conflict'];
2980*9ccd446eSAtari911        $conflictingWith = isset($event['conflictingWith']) ? $event['conflictingWith'] : [];
2981*9ccd446eSAtari911
2982*9ccd446eSAtari911        // Build conflict list for tooltip
2983*9ccd446eSAtari911        $conflictList = [];
2984*9ccd446eSAtari911        if ($hasConflict && !empty($conflictingWith)) {
2985*9ccd446eSAtari911            foreach ($conflictingWith as $conf) {
2986*9ccd446eSAtari911                $confTime = $this->formatTimeDisplay($conf['time'], isset($conf['end_time']) ? $conf['end_time'] : '');
2987*9ccd446eSAtari911                $conflictList[] = $conf['title'] . ' (' . $confTime . ')';
2988*9ccd446eSAtari911            }
2989*9ccd446eSAtari911        }
2990*9ccd446eSAtari911
2991*9ccd446eSAtari911        // No background on individual events (transparent)
2992*9ccd446eSAtari911        // Use theme grid_border with slight opacity for subtle divider
2993*9ccd446eSAtari911        $borderColor = $themeStyles['grid_border'];
2994*9ccd446eSAtari911
2995*9ccd446eSAtari911        $html = '<div style="padding:4px 6px; border-bottom:1px solid ' . $borderColor . ' !important; font-size:10px; display:flex; align-items:stretch; gap:6px; min-height:20px;">';
29961d05cddcSAtari911
2997231d0edbSAtari911        // Event's assigned color bar (single bar on the left)
2998*9ccd446eSAtari911        $barShadow = ($theme === 'professional') ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . $eventColor;
2999*9ccd446eSAtari911        $html .= '<div style="width:3px; align-self:stretch; background:' . $eventColor . '; border-radius:1px; flex-shrink:0; box-shadow:' . $barShadow . ';"></div>';
30001d05cddcSAtari911
30011d05cddcSAtari911        // Content
30021d05cddcSAtari911        $html .= '<div style="flex:1; min-width:0;">';
30031d05cddcSAtari911
30041d05cddcSAtari911        // Time + title
3005*9ccd446eSAtari911        $html .= '<div style="font-weight:600; color:' . $titleColor . '; word-wrap:break-word; font-family:system-ui, sans-serif; ' . $textShadow . '">';
30061d05cddcSAtari911
30071d05cddcSAtari911        if ($time) {
30081d05cddcSAtari911            $displayTime = $this->formatTimeDisplay($time, $endTime);
3009*9ccd446eSAtari911            $html .= '<span style="color:' . $timeColor . '; font-weight:500; font-size:9px;">' . htmlspecialchars($displayTime) . '</span> ';
30101d05cddcSAtari911        }
30111d05cddcSAtari911
30121d05cddcSAtari911        // Task checkbox
30131d05cddcSAtari911        if ($isTask) {
30141d05cddcSAtari911            $checkIcon = $completed ? '☑' : '☐';
3015*9ccd446eSAtari911            $checkColor = $themeStyles ? $themeStyles['text_bright'] : '#00ff00';
3016*9ccd446eSAtari911            $html .= '<span style="font-size:11px; color:' . $checkColor . ';">' . $checkIcon . '</span> ';
30171d05cddcSAtari911        }
30181d05cddcSAtari911
3019*9ccd446eSAtari911        $html .= $title; // Already HTML-escaped on line 2625
30201d05cddcSAtari911
3021*9ccd446eSAtari911        // Conflict badge using same system as main calendar
3022*9ccd446eSAtari911        if ($hasConflict && !empty($conflictList)) {
3023*9ccd446eSAtari911            $conflictJson = base64_encode(json_encode($conflictList));
3024*9ccd446eSAtari911            $html .= ' <span class="event-conflict-badge" style="font-size:10px;" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . count($conflictList) . '</span>';
30251d05cddcSAtari911        }
30261d05cddcSAtari911
30271d05cddcSAtari911        $html .= '</div>';
30281d05cddcSAtari911
30291d05cddcSAtari911        // Date display BELOW event name for Important events
30301d05cddcSAtari911        if ($showDate && $date) {
30311d05cddcSAtari911            $dateObj = new DateTime($date);
30321d05cddcSAtari911            $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10"
3033*9ccd446eSAtari911            $dateColor = $themeStyles ? $themeStyles['text_dim'] : '#00aa00';
3034*9ccd446eSAtari911            $dateShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $dateColor . ';' : '';
3035*9ccd446eSAtari911            $html .= '<div style="font-size:8px; color:' . $dateColor . '; font-weight:500; margin-top:2px; ' . $dateShadow . '">' . htmlspecialchars($displayDate) . '</div>';
30361d05cddcSAtari911        }
30371d05cddcSAtari911
30381d05cddcSAtari911        $html .= '</div>';
30391d05cddcSAtari911        $html .= '</div>';
30401d05cddcSAtari911
30411d05cddcSAtari911        return $html;
30421d05cddcSAtari911    }
30431d05cddcSAtari911
30441d05cddcSAtari911    /**
30451d05cddcSAtari911     * Format time display (12-hour format with optional end time)
30461d05cddcSAtari911     */
30471d05cddcSAtari911    private function formatTimeDisplay($startTime, $endTime = '') {
30481d05cddcSAtari911        // Convert start time
30491d05cddcSAtari911        list($hour, $minute) = explode(':', $startTime);
30501d05cddcSAtari911        $hour = (int)$hour;
30511d05cddcSAtari911        $ampm = $hour >= 12 ? 'PM' : 'AM';
30521d05cddcSAtari911        $displayHour = $hour % 12;
30531d05cddcSAtari911        if ($displayHour === 0) $displayHour = 12;
30541d05cddcSAtari911
30551d05cddcSAtari911        $display = $displayHour . ':' . $minute . ' ' . $ampm;
30561d05cddcSAtari911
30571d05cddcSAtari911        // Add end time if provided
30581d05cddcSAtari911        if ($endTime && $endTime !== '') {
30591d05cddcSAtari911            list($endHour, $endMinute) = explode(':', $endTime);
30601d05cddcSAtari911            $endHour = (int)$endHour;
30611d05cddcSAtari911            $endAmpm = $endHour >= 12 ? 'PM' : 'AM';
30621d05cddcSAtari911            $endDisplayHour = $endHour % 12;
30631d05cddcSAtari911            if ($endDisplayHour === 0) $endDisplayHour = 12;
30641d05cddcSAtari911
30651d05cddcSAtari911            $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm;
30661d05cddcSAtari911        }
30671d05cddcSAtari911
30681d05cddcSAtari911        return $display;
30691d05cddcSAtari911    }
30701d05cddcSAtari911
30711d05cddcSAtari911    /**
3072*9ccd446eSAtari911     * Detect time conflicts among events on the same day
3073*9ccd446eSAtari911     * Returns events array with 'conflict' flag and 'conflictingWith' array
3074*9ccd446eSAtari911     */
3075*9ccd446eSAtari911    private function detectTimeConflicts($dayEvents) {
3076*9ccd446eSAtari911        if (empty($dayEvents)) {
3077*9ccd446eSAtari911            return $dayEvents;
3078*9ccd446eSAtari911        }
3079*9ccd446eSAtari911
3080*9ccd446eSAtari911        // If only 1 event, no conflicts possible but still add the flag
3081*9ccd446eSAtari911        if (count($dayEvents) === 1) {
3082*9ccd446eSAtari911            return [array_merge($dayEvents[0], ['conflict' => false, 'conflictingWith' => []])];
3083*9ccd446eSAtari911        }
3084*9ccd446eSAtari911
3085*9ccd446eSAtari911        $eventsWithFlags = [];
3086*9ccd446eSAtari911
3087*9ccd446eSAtari911        foreach ($dayEvents as $i => $event) {
3088*9ccd446eSAtari911            $hasConflict = false;
3089*9ccd446eSAtari911            $conflictingWith = [];
3090*9ccd446eSAtari911
3091*9ccd446eSAtari911            // Skip all-day events (no time)
3092*9ccd446eSAtari911            if (empty($event['time'])) {
3093*9ccd446eSAtari911                $eventsWithFlags[] = array_merge($event, ['conflict' => false, 'conflictingWith' => []]);
3094*9ccd446eSAtari911                continue;
3095*9ccd446eSAtari911            }
3096*9ccd446eSAtari911
3097*9ccd446eSAtari911            // Get this event's time range
3098*9ccd446eSAtari911            $startTime = $event['time'];
3099*9ccd446eSAtari911            // Check both 'end_time' (snake_case) and 'endTime' (camelCase) for compatibility
3100*9ccd446eSAtari911            $endTime = '';
3101*9ccd446eSAtari911            if (isset($event['end_time']) && $event['end_time'] !== '') {
3102*9ccd446eSAtari911                $endTime = $event['end_time'];
3103*9ccd446eSAtari911            } elseif (isset($event['endTime']) && $event['endTime'] !== '') {
3104*9ccd446eSAtari911                $endTime = $event['endTime'];
3105*9ccd446eSAtari911            } else {
3106*9ccd446eSAtari911                // If no end time, use start time (zero duration) - matches main calendar logic
3107*9ccd446eSAtari911                $endTime = $startTime;
3108*9ccd446eSAtari911            }
3109*9ccd446eSAtari911
3110*9ccd446eSAtari911            // Check against all other events
3111*9ccd446eSAtari911            foreach ($dayEvents as $j => $otherEvent) {
3112*9ccd446eSAtari911                if ($i === $j) continue; // Skip self
3113*9ccd446eSAtari911                if (empty($otherEvent['time'])) continue; // Skip all-day events
3114*9ccd446eSAtari911
3115*9ccd446eSAtari911                $otherStart = $otherEvent['time'];
3116*9ccd446eSAtari911                // Check both field name formats
3117*9ccd446eSAtari911                $otherEnd = '';
3118*9ccd446eSAtari911                if (isset($otherEvent['end_time']) && $otherEvent['end_time'] !== '') {
3119*9ccd446eSAtari911                    $otherEnd = $otherEvent['end_time'];
3120*9ccd446eSAtari911                } elseif (isset($otherEvent['endTime']) && $otherEvent['endTime'] !== '') {
3121*9ccd446eSAtari911                    $otherEnd = $otherEvent['endTime'];
3122*9ccd446eSAtari911                } else {
3123*9ccd446eSAtari911                    $otherEnd = $otherStart;
3124*9ccd446eSAtari911                }
3125*9ccd446eSAtari911
3126*9ccd446eSAtari911                // Check for overlap: convert to minutes and compare
3127*9ccd446eSAtari911                $start1Min = $this->timeToMinutes($startTime);
3128*9ccd446eSAtari911                $end1Min = $this->timeToMinutes($endTime);
3129*9ccd446eSAtari911                $start2Min = $this->timeToMinutes($otherStart);
3130*9ccd446eSAtari911                $end2Min = $this->timeToMinutes($otherEnd);
3131*9ccd446eSAtari911
3132*9ccd446eSAtari911                // Overlap if: start1 < end2 AND start2 < end1
3133*9ccd446eSAtari911                // Note: Using < (not <=) so events that just touch at boundaries don't conflict
3134*9ccd446eSAtari911                // e.g., 1:00-2:00 and 2:00-3:00 are NOT in conflict
3135*9ccd446eSAtari911                if ($start1Min < $end2Min && $start2Min < $end1Min) {
3136*9ccd446eSAtari911                    $hasConflict = true;
3137*9ccd446eSAtari911                    $conflictingWith[] = [
3138*9ccd446eSAtari911                        'title' => isset($otherEvent['title']) ? $otherEvent['title'] : 'Untitled',
3139*9ccd446eSAtari911                        'time' => $otherStart,
3140*9ccd446eSAtari911                        'end_time' => $otherEnd
3141*9ccd446eSAtari911                    ];
3142*9ccd446eSAtari911                }
3143*9ccd446eSAtari911            }
3144*9ccd446eSAtari911
3145*9ccd446eSAtari911            $eventsWithFlags[] = array_merge($event, [
3146*9ccd446eSAtari911                'conflict' => $hasConflict,
3147*9ccd446eSAtari911                'conflictingWith' => $conflictingWith
3148*9ccd446eSAtari911            ]);
3149*9ccd446eSAtari911        }
3150*9ccd446eSAtari911
3151*9ccd446eSAtari911        return $eventsWithFlags;
3152*9ccd446eSAtari911    }
3153*9ccd446eSAtari911
3154*9ccd446eSAtari911    /**
3155*9ccd446eSAtari911     * Add hours to a time string
3156*9ccd446eSAtari911     */
3157*9ccd446eSAtari911    private function addHoursToTime($time, $hours) {
3158*9ccd446eSAtari911        $totalMinutes = $this->timeToMinutes($time) + ($hours * 60);
3159*9ccd446eSAtari911        $h = floor($totalMinutes / 60) % 24;
3160*9ccd446eSAtari911        $m = $totalMinutes % 60;
3161*9ccd446eSAtari911        return sprintf('%02d:%02d', $h, $m);
3162*9ccd446eSAtari911    }
3163*9ccd446eSAtari911
3164*9ccd446eSAtari911    /**
31651d05cddcSAtari911     * Render DokuWiki syntax to HTML
31661d05cddcSAtari911     * Converts **bold**, //italic//, [[links]], etc. to HTML
31671d05cddcSAtari911     */
31681d05cddcSAtari911    private function renderDokuWikiToHtml($text) {
31691d05cddcSAtari911        if (empty($text)) return '';
31701d05cddcSAtari911
31711d05cddcSAtari911        // Use DokuWiki's parser to render the text
31721d05cddcSAtari911        $instructions = p_get_instructions($text);
31731d05cddcSAtari911
31741d05cddcSAtari911        // Render instructions to XHTML
31751d05cddcSAtari911        $xhtml = p_render('xhtml', $instructions, $info);
31761d05cddcSAtari911
31771d05cddcSAtari911        // Remove surrounding <p> tags if present (we're rendering inline)
31781d05cddcSAtari911        $xhtml = preg_replace('/^<p>(.*)<\/p>$/s', '$1', trim($xhtml));
31791d05cddcSAtari911
31801d05cddcSAtari911        return $xhtml;
31811d05cddcSAtari911    }
31821d05cddcSAtari911
31831d05cddcSAtari911    // Keep old scanForNamespaces for backward compatibility (not used anymore)
31841d05cddcSAtari911    private function scanForNamespaces($dir, $baseNamespace, &$namespaces) {
31851d05cddcSAtari911        if (!is_dir($dir)) return;
31861d05cddcSAtari911
31871d05cddcSAtari911        $items = scandir($dir);
31881d05cddcSAtari911        foreach ($items as $item) {
31891d05cddcSAtari911            if ($item === '.' || $item === '..' || $item === 'calendar') continue;
31901d05cddcSAtari911
31911d05cddcSAtari911            $path = $dir . $item;
31921d05cddcSAtari911            if (is_dir($path)) {
31931d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
31941d05cddcSAtari911                $namespaces[] = $namespace;
31951d05cddcSAtari911                $this->scanForNamespaces($path . '/', $namespace, $namespaces);
31961d05cddcSAtari911            }
31971d05cddcSAtari911        }
31981d05cddcSAtari911    }
3199*9ccd446eSAtari911
3200*9ccd446eSAtari911    /**
3201*9ccd446eSAtari911     * Get current sidebar theme
3202*9ccd446eSAtari911     */
3203*9ccd446eSAtari911    private function getSidebarTheme() {
3204*9ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_theme.txt';
3205*9ccd446eSAtari911        if (file_exists($configFile)) {
3206*9ccd446eSAtari911            $theme = trim(file_get_contents($configFile));
3207*9ccd446eSAtari911            if (in_array($theme, ['matrix', 'purple', 'professional', 'pink', 'wiki'])) {
3208*9ccd446eSAtari911                return $theme;
3209*9ccd446eSAtari911            }
3210*9ccd446eSAtari911        }
3211*9ccd446eSAtari911        return 'matrix'; // Default
3212*9ccd446eSAtari911    }
3213*9ccd446eSAtari911
3214*9ccd446eSAtari911    /**
3215*9ccd446eSAtari911     * Get colors from DokuWiki template's style.ini file
3216*9ccd446eSAtari911     */
3217*9ccd446eSAtari911    private function getWikiTemplateColors() {
3218*9ccd446eSAtari911        global $conf;
3219*9ccd446eSAtari911
3220*9ccd446eSAtari911        // Get current template name
3221*9ccd446eSAtari911        $template = $conf['template'];
3222*9ccd446eSAtari911
3223*9ccd446eSAtari911        // Try multiple possible locations for style.ini
3224*9ccd446eSAtari911        $possiblePaths = [
3225*9ccd446eSAtari911            DOKU_INC . 'conf/tpl/' . $template . '/style.ini',
3226*9ccd446eSAtari911            DOKU_INC . 'lib/tpl/' . $template . '/style.ini',
3227*9ccd446eSAtari911        ];
3228*9ccd446eSAtari911
3229*9ccd446eSAtari911        $styleIni = null;
3230*9ccd446eSAtari911        foreach ($possiblePaths as $path) {
3231*9ccd446eSAtari911            if (file_exists($path)) {
3232*9ccd446eSAtari911                $styleIni = parse_ini_file($path, true);
3233*9ccd446eSAtari911                break;
3234*9ccd446eSAtari911            }
3235*9ccd446eSAtari911        }
3236*9ccd446eSAtari911
3237*9ccd446eSAtari911        if (!$styleIni) {
3238*9ccd446eSAtari911            return null; // Fall back to CSS variables
3239*9ccd446eSAtari911        }
3240*9ccd446eSAtari911
3241*9ccd446eSAtari911        // Extract color replacements
3242*9ccd446eSAtari911        $replacements = isset($styleIni['replacements']) ? $styleIni['replacements'] : [];
3243*9ccd446eSAtari911
3244*9ccd446eSAtari911        // Map style.ini colors to our theme structure
3245*9ccd446eSAtari911        $bgSite = isset($replacements['__background_site__']) ? $replacements['__background_site__'] : '#f5f5f5';
3246*9ccd446eSAtari911        $background = isset($replacements['__background__']) ? $replacements['__background__'] : '#fff';
3247*9ccd446eSAtari911        $bgAlt = isset($replacements['__background_alt__']) ? $replacements['__background_alt__'] : '#e8e8e8';
3248*9ccd446eSAtari911        $bgNeu = isset($replacements['__background_neu__']) ? $replacements['__background_neu__'] : '#eee';
3249*9ccd446eSAtari911        $text = isset($replacements['__text__']) ? $replacements['__text__'] : '#333';
3250*9ccd446eSAtari911        $textAlt = isset($replacements['__text_alt__']) ? $replacements['__text_alt__'] : '#999';
3251*9ccd446eSAtari911        $textNeu = isset($replacements['__text_neu__']) ? $replacements['__text_neu__'] : '#666';
3252*9ccd446eSAtari911        $border = isset($replacements['__border__']) ? $replacements['__border__'] : '#ccc';
3253*9ccd446eSAtari911        $link = isset($replacements['__link__']) ? $replacements['__link__'] : '#2b73b7';
3254*9ccd446eSAtari911        $existing = isset($replacements['__existing__']) ? $replacements['__existing__'] : $link;
3255*9ccd446eSAtari911
3256*9ccd446eSAtari911        // Build theme colors from template colors
3257*9ccd446eSAtari911        // ============================================
3258*9ccd446eSAtari911        // DokuWiki style.ini → Calendar CSS Variable Mapping
3259*9ccd446eSAtari911        // ============================================
3260*9ccd446eSAtari911        //   style.ini key         → CSS variable          → Used for
3261*9ccd446eSAtari911        //   __background_site__   → --background-site     → Container, panel backgrounds
3262*9ccd446eSAtari911        //   __background__        → --cell-bg             → Cell/input backgrounds (typically white)
3263*9ccd446eSAtari911        //   __background_alt__    → --background-alt      → Hover states, header backgrounds
3264*9ccd446eSAtari911        //                         → --background-header
3265*9ccd446eSAtari911        //   __background_neu__    → --cell-today-bg       → Today cell highlight
3266*9ccd446eSAtari911        //   __text__              → --text-primary        → Primary text, labels, titles
3267*9ccd446eSAtari911        //   __text_neu__          → --text-dim            → Secondary text, dates, descriptions
3268*9ccd446eSAtari911        //   __text_alt__          → (not mapped)          → Available for future use
3269*9ccd446eSAtari911        //   __border__            → --border-color        → Grid lines, input borders
3270*9ccd446eSAtari911        //                         → --header-border
3271*9ccd446eSAtari911        //   __link__              → --border-main         → Accent color: buttons, badges, active elements
3272*9ccd446eSAtari911        //                         → --text-bright         → Links, accent text
3273*9ccd446eSAtari911        //   __existing__          → (fallback to __link__)→ Available for future use
3274*9ccd446eSAtari911        //
3275*9ccd446eSAtari911        // To customize: edit your template's conf/style.ini [replacements]
3276*9ccd446eSAtari911        return [
3277*9ccd446eSAtari911            'bg' => $bgSite,
3278*9ccd446eSAtari911            'border' => $link,           // Accent color from template links
3279*9ccd446eSAtari911            'shadow' => 'rgba(0, 0, 0, 0.1)',
3280*9ccd446eSAtari911            'header_bg' => $bgAlt,       // Headers use alt background
3281*9ccd446eSAtari911            'header_border' => $border,
3282*9ccd446eSAtari911            'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
3283*9ccd446eSAtari911            'text_primary' => $text,
3284*9ccd446eSAtari911            'text_bright' => $link,
3285*9ccd446eSAtari911            'text_dim' => $textNeu,
3286*9ccd446eSAtari911            'grid_bg' => $bgSite,
3287*9ccd446eSAtari911            'grid_border' => $border,
3288*9ccd446eSAtari911            'cell_bg' => $background,    // Cells use __background__ (white/light)
3289*9ccd446eSAtari911            'cell_today_bg' => $bgNeu,
3290*9ccd446eSAtari911            'bar_glow' => '0 1px 2px',
3291*9ccd446eSAtari911        ];
3292*9ccd446eSAtari911    }
3293*9ccd446eSAtari911
3294*9ccd446eSAtari911    /**
3295*9ccd446eSAtari911     * Get theme-specific color styles
3296*9ccd446eSAtari911     */
3297*9ccd446eSAtari911    private function getSidebarThemeStyles($theme) {
3298*9ccd446eSAtari911        // For wiki theme, try to read colors from template's style.ini
3299*9ccd446eSAtari911        if ($theme === 'wiki') {
3300*9ccd446eSAtari911            $wikiColors = $this->getWikiTemplateColors();
3301*9ccd446eSAtari911            if (!empty($wikiColors)) {
3302*9ccd446eSAtari911                return $wikiColors;
3303*9ccd446eSAtari911            }
3304*9ccd446eSAtari911            // Fall through to default wiki colors if reading fails
3305*9ccd446eSAtari911        }
3306*9ccd446eSAtari911
3307*9ccd446eSAtari911        $themes = [
3308*9ccd446eSAtari911            'matrix' => [
3309*9ccd446eSAtari911                'bg' => '#242424',
3310*9ccd446eSAtari911                'border' => '#00cc07',
3311*9ccd446eSAtari911                'shadow' => 'rgba(0, 204, 7, 0.3)',
3312*9ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2a2a2a 0%, #242424 100%)',
3313*9ccd446eSAtari911                'header_border' => '#00cc07',
3314*9ccd446eSAtari911                'header_shadow' => '0 2px 8px rgba(0, 204, 7, 0.3)',
3315*9ccd446eSAtari911                'text_primary' => '#00cc07',
3316*9ccd446eSAtari911                'text_bright' => '#00ff00',
3317*9ccd446eSAtari911                'text_dim' => '#00aa00',
3318*9ccd446eSAtari911                'grid_bg' => '#1a3d1a',
3319*9ccd446eSAtari911                'grid_border' => '#00cc07',
3320*9ccd446eSAtari911                'cell_bg' => '#242424',
3321*9ccd446eSAtari911                'cell_today_bg' => '#2a4d2a',
3322*9ccd446eSAtari911                'bar_glow' => '0 0 3px',
3323*9ccd446eSAtari911            ],
3324*9ccd446eSAtari911            'purple' => [
3325*9ccd446eSAtari911                'bg' => '#2a2030',
3326*9ccd446eSAtari911                'border' => '#9b59b6',
3327*9ccd446eSAtari911                'shadow' => 'rgba(155, 89, 182, 0.3)',
3328*9ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2f2438 0%, #2a2030 100%)',
3329*9ccd446eSAtari911                'header_border' => '#9b59b6',
3330*9ccd446eSAtari911                'header_shadow' => '0 2px 8px rgba(155, 89, 182, 0.3)',
3331*9ccd446eSAtari911                'text_primary' => '#b19cd9',
3332*9ccd446eSAtari911                'text_bright' => '#d4a5ff',
3333*9ccd446eSAtari911                'text_dim' => '#8e7ab8',
3334*9ccd446eSAtari911                'grid_bg' => '#3d2b4d',
3335*9ccd446eSAtari911                'grid_border' => '#9b59b6',
3336*9ccd446eSAtari911                'cell_bg' => '#2a2030',
3337*9ccd446eSAtari911                'cell_today_bg' => '#3d2b4d',
3338*9ccd446eSAtari911                'bar_glow' => '0 0 3px',
3339*9ccd446eSAtari911            ],
3340*9ccd446eSAtari911            'professional' => [
3341*9ccd446eSAtari911                'bg' => '#f5f7fa',
3342*9ccd446eSAtari911                'border' => '#4a90e2',
3343*9ccd446eSAtari911                'shadow' => 'rgba(74, 144, 226, 0.2)',
3344*9ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #ffffff 0%, #f5f7fa 100%)',
3345*9ccd446eSAtari911                'header_border' => '#4a90e2',
3346*9ccd446eSAtari911                'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
3347*9ccd446eSAtari911                'text_primary' => '#2c3e50',
3348*9ccd446eSAtari911                'text_bright' => '#4a90e2',
3349*9ccd446eSAtari911                'text_dim' => '#7f8c8d',
3350*9ccd446eSAtari911                'grid_bg' => '#e8ecf1',
3351*9ccd446eSAtari911                'grid_border' => '#d0d7de',
3352*9ccd446eSAtari911                'cell_bg' => '#ffffff',
3353*9ccd446eSAtari911                'cell_today_bg' => '#dce8f7',
3354*9ccd446eSAtari911                'bar_glow' => '0 1px 2px',
3355*9ccd446eSAtari911            ],
3356*9ccd446eSAtari911            'pink' => [
3357*9ccd446eSAtari911                'bg' => '#1a0d14',
3358*9ccd446eSAtari911                'border' => '#ff1493',
3359*9ccd446eSAtari911                'shadow' => 'rgba(255, 20, 147, 0.4)',
3360*9ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2d1a24 0%, #1a0d14 100%)',
3361*9ccd446eSAtari911                'header_border' => '#ff1493',
3362*9ccd446eSAtari911                'header_shadow' => '0 0 12px rgba(255, 20, 147, 0.6)',
3363*9ccd446eSAtari911                'text_primary' => '#ff69b4',
3364*9ccd446eSAtari911                'text_bright' => '#ff1493',
3365*9ccd446eSAtari911                'text_dim' => '#ff85c1',
3366*9ccd446eSAtari911                'grid_bg' => '#2d1a24',
3367*9ccd446eSAtari911                'grid_border' => '#ff1493',
3368*9ccd446eSAtari911                'cell_bg' => '#1a0d14',
3369*9ccd446eSAtari911                'cell_today_bg' => '#3d2030',
3370*9ccd446eSAtari911                'bar_glow' => '0 0 5px',
3371*9ccd446eSAtari911            ],
3372*9ccd446eSAtari911            'wiki' => [
3373*9ccd446eSAtari911                'bg' => '#f5f5f5',
3374*9ccd446eSAtari911                'border' => '#2b73b7',       // Use link blue as accent (matches template)
3375*9ccd446eSAtari911                'shadow' => 'rgba(0, 0, 0, 0.1)',
3376*9ccd446eSAtari911                'header_bg' => '#e8e8e8',
3377*9ccd446eSAtari911                'header_border' => '#ccc',
3378*9ccd446eSAtari911                'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
3379*9ccd446eSAtari911                'text_primary' => '#333',
3380*9ccd446eSAtari911                'text_bright' => '#2b73b7',
3381*9ccd446eSAtari911                'text_dim' => '#666',
3382*9ccd446eSAtari911                'grid_bg' => '#f5f5f5',
3383*9ccd446eSAtari911                'grid_border' => '#ccc',
3384*9ccd446eSAtari911                'cell_bg' => '#fff',
3385*9ccd446eSAtari911                'cell_today_bg' => '#eee',
3386*9ccd446eSAtari911                'bar_glow' => '0 1px 2px',
3387*9ccd446eSAtari911            ],
3388*9ccd446eSAtari911        ];
3389*9ccd446eSAtari911
3390*9ccd446eSAtari911        return isset($themes[$theme]) ? $themes[$theme] : $themes['matrix'];
3391*9ccd446eSAtari911    }
3392*9ccd446eSAtari911
3393*9ccd446eSAtari911    /**
3394*9ccd446eSAtari911     * Get week start day preference
3395*9ccd446eSAtari911     */
3396*9ccd446eSAtari911    private function getWeekStartDay() {
3397*9ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt';
3398*9ccd446eSAtari911        if (file_exists($configFile)) {
3399*9ccd446eSAtari911            $start = trim(file_get_contents($configFile));
3400*9ccd446eSAtari911            if (in_array($start, ['monday', 'sunday'])) {
3401*9ccd446eSAtari911                return $start;
3402*9ccd446eSAtari911            }
3403*9ccd446eSAtari911        }
3404*9ccd446eSAtari911        return 'sunday'; // Default to Sunday (US/Canada standard)
3405*9ccd446eSAtari911    }
340619378907SAtari911}