xref: /plugin/calendar/syntax.php (revision 96df7d3e9a825dddf459ab1ee6077a9886837f17)
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
737e8ea635SAtari911        // Disable caching - theme can change via admin without page edit
747e8ea635SAtari911        $renderer->nocache();
757e8ea635SAtari911
7619378907SAtari911        if ($data['type'] === 'eventlist') {
7719378907SAtari911            $html = $this->renderStandaloneEventList($data);
7819378907SAtari911        } elseif ($data['type'] === 'eventpanel') {
7919378907SAtari911            $html = $this->renderEventPanelOnly($data);
8019378907SAtari911        } else {
8119378907SAtari911            $html = $this->renderCompactCalendar($data);
8219378907SAtari911        }
8319378907SAtari911
8419378907SAtari911        $renderer->doc .= $html;
8519378907SAtari911        return true;
8619378907SAtari911    }
8719378907SAtari911
8819378907SAtari911    private function renderCompactCalendar($data) {
8919378907SAtari911        $year = (int)$data['year'];
9019378907SAtari911        $month = (int)$data['month'];
9119378907SAtari911        $namespace = $data['namespace'];
9219378907SAtari911
930c3b6e81SAtari911        // Get theme - prefer inline theme= parameter, fall back to admin default
940c3b6e81SAtari911        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();
959ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
969ccd446eSAtari911        $themeClass = 'calendar-theme-' . $theme;
979ccd446eSAtari911
989ccd446eSAtari911        // Determine button text color: professional uses white, others use bg color
999ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
1009ccd446eSAtari911
101e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
102e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
103e3a9f44cSAtari911
104e3a9f44cSAtari911        if ($isMultiNamespace) {
105e3a9f44cSAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
106e3a9f44cSAtari911        } else {
10719378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
108e3a9f44cSAtari911        }
10919378907SAtari911        $calId = 'cal_' . md5(serialize($data) . microtime());
11019378907SAtari911
11119378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
11219378907SAtari911
11319378907SAtari911        $prevMonth = $month - 1;
11419378907SAtari911        $prevYear = $year;
11519378907SAtari911        if ($prevMonth < 1) {
11619378907SAtari911            $prevMonth = 12;
11719378907SAtari911            $prevYear--;
11819378907SAtari911        }
11919378907SAtari911
12019378907SAtari911        $nextMonth = $month + 1;
12119378907SAtari911        $nextYear = $year;
12219378907SAtari911        if ($nextMonth > 12) {
12319378907SAtari911            $nextMonth = 1;
12419378907SAtari911            $nextYear++;
12519378907SAtari911        }
12619378907SAtari911
127*96df7d3eSAtari911        // Get important namespaces from config for highlighting
128*96df7d3eSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
129*96df7d3eSAtari911        $importantNsList = ['important']; // default
130*96df7d3eSAtari911        if (file_exists($configFile)) {
131*96df7d3eSAtari911            $config = include $configFile;
132*96df7d3eSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
133*96df7d3eSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
134*96df7d3eSAtari911            }
135*96df7d3eSAtari911        }
136*96df7d3eSAtari911
1379ccd446eSAtari911        // Container - all styling via CSS variables
138*96df7d3eSAtari911        $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)) . '" data-important-namespaces="' . htmlspecialchars(json_encode($importantNsList)) . '">';
1399ccd446eSAtari911
1409ccd446eSAtari911        // Inject CSS variables for this calendar instance - all theming flows from here
1419ccd446eSAtari911        $html .= '<style>
1429ccd446eSAtari911        #' . $calId . ' {
1439ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
1449ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
1459ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
1469ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
1479ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
1489ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
1499ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
1509ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
1519ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
1529ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
1539ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
1549ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
1559ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
1569ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
1579ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
1587e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
1597e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
1607e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
1617e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
1627e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
1637e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
1647e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
1659ccd446eSAtari911        }
1669ccd446eSAtari911        #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
1679ccd446eSAtari911        #event-search-' . $calId . '::-webkit-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
1689ccd446eSAtari911        #event-search-' . $calId . '::-moz-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
1699ccd446eSAtari911        #event-search-' . $calId . ':-ms-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
1709ccd446eSAtari911        </style>';
1711d05cddcSAtari911
1721d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
1731d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
1741d05cddcSAtari911
1751d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
1761d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
17719378907SAtari911
17819378907SAtari911        // Embed events data as JSON for JavaScript access
17919378907SAtari911        $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>';
18019378907SAtari911
18119378907SAtari911        // Left side: Calendar
18219378907SAtari911        $html .= '<div class="calendar-compact-left">';
18319378907SAtari911
18419378907SAtari911        // Header with navigation
18519378907SAtari911        $html .= '<div class="calendar-compact-header">';
18619378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
18787ac9bf3SAtari911        $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . '</h3>';
18819378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
18987ac9bf3SAtari911        $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
19019378907SAtari911        $html .= '</div>';
19119378907SAtari911
1920c3b6e81SAtari911        // Calendar grid - day name headers as a separate div (avoids Firefox th height issues)
1930c3b6e81SAtari911        $html .= '<div class="calendar-day-headers">';
1940c3b6e81SAtari911        $html .= '<span>S</span><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span>';
1950c3b6e81SAtari911        $html .= '</div>';
19619378907SAtari911        $html .= '<table class="calendar-compact-grid">';
1970c3b6e81SAtari911        $html .= '<tbody>';
19819378907SAtari911
19919378907SAtari911        $firstDay = mktime(0, 0, 0, $month, 1, $year);
20019378907SAtari911        $daysInMonth = date('t', $firstDay);
20119378907SAtari911        $dayOfWeek = date('w', $firstDay);
20219378907SAtari911
203e3a9f44cSAtari911        // Build a map of all events with their date ranges for the calendar grid
20487ac9bf3SAtari911        $eventRanges = array();
205e3a9f44cSAtari911        foreach ($events as $dateKey => $dayEvents) {
20687ac9bf3SAtari911            foreach ($dayEvents as $evt) {
20787ac9bf3SAtari911                $eventId = isset($evt['id']) ? $evt['id'] : '';
20887ac9bf3SAtari911                $startDate = $dateKey;
20987ac9bf3SAtari911                $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey;
21087ac9bf3SAtari911
21187ac9bf3SAtari911                // Only process events that touch this month
21287ac9bf3SAtari911                $eventStart = new DateTime($startDate);
21387ac9bf3SAtari911                $eventEnd = new DateTime($endDate);
21487ac9bf3SAtari911                $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month));
21587ac9bf3SAtari911                $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth));
21687ac9bf3SAtari911
21787ac9bf3SAtari911                // Skip if event doesn't overlap with current month
21887ac9bf3SAtari911                if ($eventEnd < $monthStart || $eventStart > $monthEnd) {
21987ac9bf3SAtari911                    continue;
22087ac9bf3SAtari911                }
22187ac9bf3SAtari911
22287ac9bf3SAtari911                // Create entry for each day the event spans
22387ac9bf3SAtari911                $current = clone $eventStart;
22487ac9bf3SAtari911                while ($current <= $eventEnd) {
22587ac9bf3SAtari911                    $currentKey = $current->format('Y-m-d');
22687ac9bf3SAtari911
22787ac9bf3SAtari911                    // Check if this date is in current month
22887ac9bf3SAtari911                    $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey);
22987ac9bf3SAtari911                    if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) {
23087ac9bf3SAtari911                        if (!isset($eventRanges[$currentKey])) {
23187ac9bf3SAtari911                            $eventRanges[$currentKey] = array();
23287ac9bf3SAtari911                        }
23387ac9bf3SAtari911
23487ac9bf3SAtari911                        // Add event with span information
23587ac9bf3SAtari911                        $evt['_span_start'] = $startDate;
23687ac9bf3SAtari911                        $evt['_span_end'] = $endDate;
23787ac9bf3SAtari911                        $evt['_is_first_day'] = ($currentKey === $startDate);
23887ac9bf3SAtari911                        $evt['_is_last_day'] = ($currentKey === $endDate);
23987ac9bf3SAtari911                        $evt['_original_date'] = $dateKey; // Keep track of original date
24087ac9bf3SAtari911
24187ac9bf3SAtari911                        // Check if event continues from previous month or to next month
24287ac9bf3SAtari911                        $evt['_continues_from_prev'] = ($eventStart < $monthStart);
24387ac9bf3SAtari911                        $evt['_continues_to_next'] = ($eventEnd > $monthEnd);
24487ac9bf3SAtari911
24587ac9bf3SAtari911                        $eventRanges[$currentKey][] = $evt;
24687ac9bf3SAtari911                    }
24787ac9bf3SAtari911
24887ac9bf3SAtari911                    $current->modify('+1 day');
24987ac9bf3SAtari911                }
25087ac9bf3SAtari911            }
25187ac9bf3SAtari911        }
25287ac9bf3SAtari911
25319378907SAtari911        $currentDay = 1;
25419378907SAtari911        $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7);
25519378907SAtari911
25619378907SAtari911        for ($row = 0; $row < $rowCount; $row++) {
25719378907SAtari911            $html .= '<tr>';
25819378907SAtari911            for ($col = 0; $col < 7; $col++) {
25919378907SAtari911                if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) {
26019378907SAtari911                    $html .= '<td class="cal-empty"></td>';
26119378907SAtari911                } else {
26219378907SAtari911                    $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay);
26319378907SAtari911                    $isToday = ($dateKey === date('Y-m-d'));
26487ac9bf3SAtari911                    $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]);
26519378907SAtari911
26619378907SAtari911                    $classes = 'cal-day';
26719378907SAtari911                    if ($isToday) $classes .= ' cal-today';
26819378907SAtari911                    if ($hasEvents) $classes .= ' cal-has-events';
26919378907SAtari911
27019378907SAtari911                    $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">';
2719ccd446eSAtari911
2729ccd446eSAtari911                    $dayNumClass = $isToday ? 'day-num day-num-today' : 'day-num';
2739ccd446eSAtari911                    $html .= '<span class="' . $dayNumClass . '">' . $currentDay . '</span>';
27419378907SAtari911
27519378907SAtari911                    if ($hasEvents) {
27619378907SAtari911                        // Sort events by time (no time first, then by time)
27787ac9bf3SAtari911                        $sortedEvents = $eventRanges[$dateKey];
27819378907SAtari911                        usort($sortedEvents, function($a, $b) {
27919378907SAtari911                            $timeA = isset($a['time']) ? $a['time'] : '';
28019378907SAtari911                            $timeB = isset($b['time']) ? $b['time'] : '';
28119378907SAtari911
28219378907SAtari911                            // Events without time go first
28319378907SAtari911                            if (empty($timeA) && !empty($timeB)) return -1;
28419378907SAtari911                            if (!empty($timeA) && empty($timeB)) return 1;
28519378907SAtari911                            if (empty($timeA) && empty($timeB)) return 0;
28619378907SAtari911
28719378907SAtari911                            // Sort by time
28819378907SAtari911                            return strcmp($timeA, $timeB);
28919378907SAtari911                        });
29019378907SAtari911
29119378907SAtari911                        // Show colored stacked bars for each event
29219378907SAtari911                        $html .= '<div class="event-indicators">';
29319378907SAtari911                        foreach ($sortedEvents as $evt) {
29419378907SAtari911                            $eventId = isset($evt['id']) ? $evt['id'] : '';
29519378907SAtari911                            $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db';
29619378907SAtari911                            $eventTime = isset($evt['time']) ? $evt['time'] : '';
29719378907SAtari911                            $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event';
29887ac9bf3SAtari911                            $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey;
29987ac9bf3SAtari911                            $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true;
30087ac9bf3SAtari911                            $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true;
30119378907SAtari911
302*96df7d3eSAtari911                            // Check if this event is from an important namespace
303*96df7d3eSAtari911                            $evtNs = isset($evt['namespace']) ? $evt['namespace'] : '';
304*96df7d3eSAtari911                            if (!$evtNs && isset($evt['_namespace'])) {
305*96df7d3eSAtari911                                $evtNs = $evt['_namespace'];
306*96df7d3eSAtari911                            }
307*96df7d3eSAtari911                            $isImportantEvent = false;
308*96df7d3eSAtari911                            foreach ($importantNsList as $impNs) {
309*96df7d3eSAtari911                                if ($evtNs === $impNs || strpos($evtNs, $impNs . ':') === 0) {
310*96df7d3eSAtari911                                    $isImportantEvent = true;
311*96df7d3eSAtari911                                    break;
312*96df7d3eSAtari911                                }
313*96df7d3eSAtari911                            }
314*96df7d3eSAtari911
31519378907SAtari911                            $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed';
31619378907SAtari911
31787ac9bf3SAtari911                            // Add classes for multi-day spanning
31887ac9bf3SAtari911                            if (!$isFirstDay) $barClass .= ' event-bar-continues';
31987ac9bf3SAtari911                            if (!$isLastDay) $barClass .= ' event-bar-continuing';
320*96df7d3eSAtari911                            if ($isImportantEvent) {
321*96df7d3eSAtari911                                $barClass .= ' event-bar-important';
322*96df7d3eSAtari911                                if ($isFirstDay) {
323*96df7d3eSAtari911                                    $barClass .= ' event-bar-has-star';
324*96df7d3eSAtari911                                }
325*96df7d3eSAtari911                            }
326*96df7d3eSAtari911
327*96df7d3eSAtari911                            $titlePrefix = $isImportantEvent ? '⭐ ' : '';
32887ac9bf3SAtari911
32919378907SAtari911                            $html .= '<span class="event-bar ' . $barClass . '" ';
33019378907SAtari911                            $html .= 'style="background: ' . $eventColor . ';" ';
331*96df7d3eSAtari911                            $html .= 'title="' . $titlePrefix . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" ';
33287ac9bf3SAtari911                            $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">';
33319378907SAtari911                            $html .= '</span>';
33419378907SAtari911                        }
33519378907SAtari911                        $html .= '</div>';
33619378907SAtari911                    }
33719378907SAtari911
33819378907SAtari911                    $html .= '</td>';
33919378907SAtari911                    $currentDay++;
34019378907SAtari911                }
34119378907SAtari911            }
34219378907SAtari911            $html .= '</tr>';
34319378907SAtari911        }
34419378907SAtari911
34519378907SAtari911        $html .= '</tbody></table>';
34619378907SAtari911        $html .= '</div>'; // End calendar-left
34719378907SAtari911
34819378907SAtari911        // Right side: Event list
34919378907SAtari911        $html .= '<div class="calendar-compact-right">';
35019378907SAtari911        $html .= '<div class="event-list-header">';
35119378907SAtari911        $html .= '<div class="event-list-header-content">';
35219378907SAtari911        $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>';
35319378907SAtari911        if ($namespace) {
35419378907SAtari911            $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>';
35519378907SAtari911        }
35619378907SAtari911        $html .= '</div>';
3571d05cddcSAtari911
3581d05cddcSAtari911        // Search bar in header
3591d05cddcSAtari911        $html .= '<div class="event-search-container-inline">';
3601d05cddcSAtari911        $html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" placeholder="�� Search..." oninput="filterEvents(\'' . $calId . '\', this.value)">';
3611d05cddcSAtari911        $html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
362*96df7d3eSAtari911        $html .= '<button class="event-search-mode-inline" id="search-mode-' . $calId . '" onclick="toggleSearchMode(\'' . $calId . '\', \'' . $namespace . '\')" title="Search this month only">��</button>';
3631d05cddcSAtari911        $html .= '</div>';
3641d05cddcSAtari911
36519378907SAtari911        $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
36619378907SAtari911        $html .= '</div>';
36719378907SAtari911
36819378907SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">';
3699ccd446eSAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace, $themeStyles);
37019378907SAtari911        $html .= '</div>';
37119378907SAtari911
37219378907SAtari911        $html .= '</div>'; // End calendar-right
37319378907SAtari911
37419378907SAtari911        // Event dialog
3750c3b6e81SAtari911        $html .= $this->renderEventDialog($calId, $namespace, $theme);
37619378907SAtari911
37787ac9bf3SAtari911        // Month/Year picker dialog (at container level for proper overlay)
3789ccd446eSAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
37987ac9bf3SAtari911
38019378907SAtari911        $html .= '</div>'; // End container
38119378907SAtari911
38219378907SAtari911        return $html;
38319378907SAtari911    }
38419378907SAtari911
3859ccd446eSAtari911    private function renderEventListContent($events, $calId, $namespace, $themeStyles = null) {
38619378907SAtari911        if (empty($events)) {
38719378907SAtari911            return '<p class="no-events-msg">No events this month</p>';
38819378907SAtari911        }
38919378907SAtari911
3909ccd446eSAtari911        // Default theme styles if not provided
3919ccd446eSAtari911        if ($themeStyles === null) {
3929ccd446eSAtari911            $theme = $this->getSidebarTheme();
3939ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
394*96df7d3eSAtari911        } else {
395*96df7d3eSAtari911            $theme = $this->getSidebarTheme();
396*96df7d3eSAtari911        }
397*96df7d3eSAtari911
398*96df7d3eSAtari911        // Get important namespaces from config
399*96df7d3eSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
400*96df7d3eSAtari911        $importantNsList = ['important']; // default
401*96df7d3eSAtari911        if (file_exists($configFile)) {
402*96df7d3eSAtari911            $config = include $configFile;
403*96df7d3eSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
404*96df7d3eSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
405*96df7d3eSAtari911            }
4069ccd446eSAtari911        }
4079ccd446eSAtari911
4081d05cddcSAtari911        // Check for time conflicts
4091d05cddcSAtari911        $events = $this->checkTimeConflicts($events);
4101d05cddcSAtari911
411e3a9f44cSAtari911        // Sort by date ascending (chronological order - oldest first)
41219378907SAtari911        ksort($events);
41319378907SAtari911
414e3a9f44cSAtari911        // Sort events within each day by time
415e3a9f44cSAtari911        foreach ($events as $dateKey => &$dayEvents) {
416e3a9f44cSAtari911            usort($dayEvents, function($a, $b) {
4171d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
4181d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
4191d05cddcSAtari911
4201d05cddcSAtari911                // All-day events (no time) go to the TOP
4211d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
4221d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
4231d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
4241d05cddcSAtari911
4251d05cddcSAtari911                // Both have times, sort chronologically
426e3a9f44cSAtari911                return strcmp($timeA, $timeB);
427e3a9f44cSAtari911            });
428e3a9f44cSAtari911        }
429e3a9f44cSAtari911        unset($dayEvents); // Break reference
430e3a9f44cSAtari911
431e3a9f44cSAtari911        // Get today's date for comparison
432e3a9f44cSAtari911        $today = date('Y-m-d');
433e3a9f44cSAtari911        $firstFutureEventId = null;
434e3a9f44cSAtari911
4351d05cddcSAtari911        // Helper function to check if event is past (with 15-minute grace period for timed events)
4361d05cddcSAtari911        $isEventPast = function($dateKey, $time) use ($today) {
4371d05cddcSAtari911            // If event is on a past date, it's definitely past
4381d05cddcSAtari911            if ($dateKey < $today) {
4391d05cddcSAtari911                return true;
4401d05cddcSAtari911            }
4411d05cddcSAtari911
4421d05cddcSAtari911            // If event is on a future date, it's definitely not past
4431d05cddcSAtari911            if ($dateKey > $today) {
4441d05cddcSAtari911                return false;
4451d05cddcSAtari911            }
4461d05cddcSAtari911
4471d05cddcSAtari911            // Event is today - check time with grace period
4481d05cddcSAtari911            if ($time && $time !== '') {
4491d05cddcSAtari911                try {
4501d05cddcSAtari911                    $currentDateTime = new DateTime();
4511d05cddcSAtari911                    $eventDateTime = new DateTime($dateKey . ' ' . $time);
4521d05cddcSAtari911
4531d05cddcSAtari911                    // Add 15-minute grace period
4541d05cddcSAtari911                    $eventDateTime->modify('+15 minutes');
4551d05cddcSAtari911
4561d05cddcSAtari911                    // Event is past if current time > event time + 15 minutes
4571d05cddcSAtari911                    return $currentDateTime > $eventDateTime;
4581d05cddcSAtari911                } catch (Exception $e) {
4591d05cddcSAtari911                    // If time parsing fails, fall back to date-only comparison
4601d05cddcSAtari911                    return false;
4611d05cddcSAtari911                }
4621d05cddcSAtari911            }
4631d05cddcSAtari911
4641d05cddcSAtari911            // No time specified for today's event, treat as future
4651d05cddcSAtari911            return false;
4661d05cddcSAtari911        };
4671d05cddcSAtari911
4681d05cddcSAtari911        // Build HTML for each event - separate past/completed from future
4691d05cddcSAtari911        $pastHtml = '';
4701d05cddcSAtari911        $futureHtml = '';
4711d05cddcSAtari911        $pastCount = 0;
472e3a9f44cSAtari911
47319378907SAtari911        foreach ($events as $dateKey => $dayEvents) {
474e3a9f44cSAtari911
47519378907SAtari911            foreach ($dayEvents as $event) {
476e3a9f44cSAtari911                // Track first future/today event for auto-scroll
477e3a9f44cSAtari911                if (!$firstFutureEventId && $dateKey >= $today) {
478e3a9f44cSAtari911                    $firstFutureEventId = isset($event['id']) ? $event['id'] : '';
479e3a9f44cSAtari911                }
48019378907SAtari911                $eventId = isset($event['id']) ? $event['id'] : '';
48119378907SAtari911                $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
4821d05cddcSAtari911                $timeRaw = isset($event['time']) ? $event['time'] : '';
4831d05cddcSAtari911                $time = htmlspecialchars($timeRaw);
4841d05cddcSAtari911                $endTime = isset($event['endTime']) ? htmlspecialchars($event['endTime']) : '';
48519378907SAtari911                $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db';
48619378907SAtari911                $description = isset($event['description']) ? $event['description'] : '';
48719378907SAtari911                $isTask = isset($event['isTask']) ? $event['isTask'] : false;
48819378907SAtari911                $completed = isset($event['completed']) ? $event['completed'] : false;
48919378907SAtari911                $endDate = isset($event['endDate']) ? $event['endDate'] : '';
49019378907SAtari911
4911d05cddcSAtari911                // Use helper function to determine if event is past (with grace period)
4921d05cddcSAtari911                $isPast = $isEventPast($dateKey, $timeRaw);
4931d05cddcSAtari911                $isToday = $dateKey === $today;
4941d05cddcSAtari911
4951d05cddcSAtari911                // Check if event should be in past section
4961d05cddcSAtari911                // EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past
4971d05cddcSAtari911                $isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed;
4981d05cddcSAtari911                if ($isPastOrCompleted) {
4991d05cddcSAtari911                    $pastCount++;
5001d05cddcSAtari911                }
5011d05cddcSAtari911
5021d05cddcSAtari911                // Determine if task is past due (past date, is task, not completed)
5031d05cddcSAtari911                $isPastDue = $isPast && $isTask && !$completed;
5041d05cddcSAtari911
50519378907SAtari911                // Process description for wiki syntax, HTML, images, and links
5069ccd446eSAtari911                $renderedDescription = $this->renderDescription($description, $themeStyles);
50719378907SAtari911
5081d05cddcSAtari911                // Convert to 12-hour format and handle time ranges
50919378907SAtari911                $displayTime = '';
51019378907SAtari911                if ($time) {
51119378907SAtari911                    $timeObj = DateTime::createFromFormat('H:i', $time);
51219378907SAtari911                    if ($timeObj) {
51319378907SAtari911                        $displayTime = $timeObj->format('g:i A');
5141d05cddcSAtari911
5151d05cddcSAtari911                        // Add end time if present and different from start time
5161d05cddcSAtari911                        if ($endTime && $endTime !== $time) {
5171d05cddcSAtari911                            $endTimeObj = DateTime::createFromFormat('H:i', $endTime);
5181d05cddcSAtari911                            if ($endTimeObj) {
5191d05cddcSAtari911                                $displayTime .= ' - ' . $endTimeObj->format('g:i A');
5201d05cddcSAtari911                            }
5211d05cddcSAtari911                        }
52219378907SAtari911                    } else {
52319378907SAtari911                        $displayTime = $time;
52419378907SAtari911                    }
52519378907SAtari911                }
52619378907SAtari911
52787ac9bf3SAtari911                // Format date display with day of week
528e3a9f44cSAtari911                // Use originalStartDate if this is a multi-month event continuation
529e3a9f44cSAtari911                $displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey;
530e3a9f44cSAtari911                $dateObj = new DateTime($displayDateKey);
53187ac9bf3SAtari911                $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24"
53219378907SAtari911
53319378907SAtari911                // Multi-day indicator
53419378907SAtari911                $multiDay = '';
535e3a9f44cSAtari911                if ($endDate && $endDate !== $displayDateKey) {
53619378907SAtari911                    $endObj = new DateTime($endDate);
53787ac9bf3SAtari911                    $multiDay = ' → ' . $endObj->format('D, M j');
53819378907SAtari911                }
53919378907SAtari911
54019378907SAtari911                $completedClass = $completed ? ' event-completed' : '';
5411d05cddcSAtari911                // Don't grey out past due tasks - they need attention!
5421d05cddcSAtari911                $pastClass = ($isPast && !$isPastDue) ? ' event-past' : '';
5431d05cddcSAtari911                $pastDueClass = $isPastDue ? ' event-pastdue' : '';
544e3a9f44cSAtari911                $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : '';
54519378907SAtari911
546*96df7d3eSAtari911                // Check if this is an important namespace event
547*96df7d3eSAtari911                $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
548*96df7d3eSAtari911                if (!$eventNamespace && isset($event['_namespace'])) {
549*96df7d3eSAtari911                    $eventNamespace = $event['_namespace'];
550*96df7d3eSAtari911                }
551*96df7d3eSAtari911                $isImportantNs = false;
552*96df7d3eSAtari911                foreach ($importantNsList as $impNs) {
553*96df7d3eSAtari911                    if ($eventNamespace === $impNs || strpos($eventNamespace, $impNs . ':') === 0) {
554*96df7d3eSAtari911                        $isImportantNs = true;
555*96df7d3eSAtari911                        break;
556*96df7d3eSAtari911                    }
557*96df7d3eSAtari911                }
558*96df7d3eSAtari911                $importantClass = $isImportantNs ? ' event-important' : '';
559*96df7d3eSAtari911
5609ccd446eSAtari911                // For all themes: use CSS variables, only keep border-left-color as inline
5619ccd446eSAtari911                $pastClickHandler = ($isPast && !$isPastDue) ? ' onclick="togglePastEventExpand(this)"' : '';
562*96df7d3eSAtari911                $eventHtml = '<div class="event-compact-item' . $completedClass . $pastClass . $pastDueClass . $importantClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ' !important;"' . $pastClickHandler . $firstFutureAttr . '>';
5631d05cddcSAtari911                $eventHtml .= '<div class="event-info">';
5649ccd446eSAtari911
5651d05cddcSAtari911                $eventHtml .= '<div class="event-title-row">';
566*96df7d3eSAtari911                // Add star for important namespace events
567*96df7d3eSAtari911                if ($isImportantNs) {
568*96df7d3eSAtari911                    $eventHtml .= '<span class="event-important-star" title="Important">⭐</span> ';
569*96df7d3eSAtari911                }
5701d05cddcSAtari911                $eventHtml .= '<span class="event-title-compact">' . $title . '</span>';
5711d05cddcSAtari911                $eventHtml .= '</div>';
57219378907SAtari911
573e3a9f44cSAtari911                // For past events, hide meta and description (collapsed)
5741d05cddcSAtari911                // EXCEPTION: Past due tasks should show their details
5751d05cddcSAtari911                if (!$isPast || $isPastDue) {
5761d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact">';
5771d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
57819378907SAtari911                    if ($displayTime) {
5791d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
58019378907SAtari911                    }
5811d05cddcSAtari911                    // Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks
5821d05cddcSAtari911                    if ($isPastDue) {
5837e8ea635SAtari911                        $eventHtml .= ' <span class="event-pastdue-badge" style="background:' . $themeStyles['pastdue_color'] . ' !important; color:white !important; -webkit-text-fill-color:white !important;">' . 'PAST DUE</span>';
5841d05cddcSAtari911                    } elseif ($isToday) {
5857e8ea635SAtari911                        $eventHtml .= ' <span class="event-today-badge" style="background:' . $themeStyles['border'] . ' !important; color:' . $themeStyles['bg'] . ' !important; -webkit-text-fill-color:' . $themeStyles['bg'] . ' !important;">' . 'TODAY</span>';
586e3a9f44cSAtari911                    }
5871d05cddcSAtari911                    // Add namespace badge - ALWAYS show if event has a namespace
588e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
589e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
590e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility
591e3a9f44cSAtari911                    }
5921d05cddcSAtari911                    // Show badge if namespace exists and is not empty
5931d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
5947e8ea635SAtari911                        $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer; background:' . $themeStyles['text_bright'] . ' !important; color:' . $themeStyles['bg'] . ' !important; -webkit-text-fill-color:' . $themeStyles['bg'] . ' !important;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>';
595e3a9f44cSAtari911                    }
5961d05cddcSAtari911
5971d05cddcSAtari911                    // Add conflict warning if event has time conflicts
5981d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
5991d05cddcSAtari911                        $conflictList = [];
6001d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
6019ccd446eSAtari911                            $conflictText = $conflict['title'];
6021d05cddcSAtari911                            if (!empty($conflict['time'])) {
6031d05cddcSAtari911                                // Format time range
6041d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
6051d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
6061d05cddcSAtari911
6071d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
6081d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
6091d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
6101d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
6111d05cddcSAtari911                                } else {
6121d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
6131d05cddcSAtari911                                }
6141d05cddcSAtari911                            }
6151d05cddcSAtari911                            $conflictList[] = $conflictText;
6161d05cddcSAtari911                        }
6171d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
6189ccd446eSAtari911                        $conflictJson = base64_encode(json_encode($conflictList));
6191d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
6201d05cddcSAtari911                    }
6211d05cddcSAtari911
6221d05cddcSAtari911                    $eventHtml .= '</span>';
6231d05cddcSAtari911                    $eventHtml .= '</div>';
62419378907SAtari911
62519378907SAtari911                    if ($description) {
6261d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>';
6271d05cddcSAtari911                    }
6281d05cddcSAtari911                } else {
6291d05cddcSAtari911                    // Past events: render with display:none for click-to-expand
6301d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact" style="display:none;">';
6311d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
6321d05cddcSAtari911                    if ($displayTime) {
6331d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
6341d05cddcSAtari911                    }
6351d05cddcSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
6361d05cddcSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
6371d05cddcSAtari911                        $eventNamespace = $event['_namespace'];
6381d05cddcSAtari911                    }
6391d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
6407e8ea635SAtari911                        $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer; background:' . $themeStyles['text_bright'] . ' !important; color:' . $themeStyles['bg'] . ' !important; -webkit-text-fill-color:' . $themeStyles['bg'] . ' !important;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>';
6411d05cddcSAtari911                    }
6421d05cddcSAtari911
6431d05cddcSAtari911                    // Add conflict warning if event has time conflicts
6441d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
6451d05cddcSAtari911                        $conflictList = [];
6461d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
6479ccd446eSAtari911                            $conflictText = $conflict['title'];
6481d05cddcSAtari911                            if (!empty($conflict['time'])) {
6491d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
6501d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
6511d05cddcSAtari911
6521d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
6531d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
6541d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
6551d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
6561d05cddcSAtari911                                } else {
6571d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
6581d05cddcSAtari911                                }
6591d05cddcSAtari911                            }
6601d05cddcSAtari911                            $conflictList[] = $conflictText;
6611d05cddcSAtari911                        }
6621d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
6639ccd446eSAtari911                        $conflictJson = base64_encode(json_encode($conflictList));
6641d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
6651d05cddcSAtari911                    }
6661d05cddcSAtari911
6671d05cddcSAtari911                    $eventHtml .= '</span>';
6681d05cddcSAtari911                    $eventHtml .= '</div>';
6691d05cddcSAtari911
6701d05cddcSAtari911                    if ($description) {
6711d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '</div>';
67219378907SAtari911                    }
673e3a9f44cSAtari911                }
67419378907SAtari911
6751d05cddcSAtari911                $eventHtml .= '</div>'; // event-info
67619378907SAtari911
677e3a9f44cSAtari911                // Use stored namespace from event, fallback to passed namespace
678e3a9f44cSAtari911                $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace;
679e3a9f44cSAtari911
6801d05cddcSAtari911                $eventHtml .= '<div class="event-actions-compact">';
6811d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">��️</button>';
6821d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>';
6831d05cddcSAtari911                $eventHtml .= '</div>';
68419378907SAtari911
68519378907SAtari911                // Checkbox for tasks - ON THE FAR RIGHT
68619378907SAtari911                if ($isTask) {
68719378907SAtari911                    $checked = $completed ? 'checked' : '';
6881d05cddcSAtari911                    $eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">';
68919378907SAtari911                }
69019378907SAtari911
6911d05cddcSAtari911                $eventHtml .= '</div>';
6921d05cddcSAtari911
6931d05cddcSAtari911                // Add to appropriate section
6941d05cddcSAtari911                if ($isPastOrCompleted) {
6951d05cddcSAtari911                    $pastHtml .= $eventHtml;
6961d05cddcSAtari911                } else {
6971d05cddcSAtari911                    $futureHtml .= $eventHtml;
6981d05cddcSAtari911                }
6991d05cddcSAtari911            }
7001d05cddcSAtari911        }
7011d05cddcSAtari911
7021d05cddcSAtari911        // Build final HTML with collapsible past events section
7031d05cddcSAtari911        $html = '';
7041d05cddcSAtari911
7051d05cddcSAtari911        // Add collapsible past events section if any exist
7061d05cddcSAtari911        if ($pastCount > 0) {
7071d05cddcSAtari911            $html .= '<div class="past-events-section">';
7081d05cddcSAtari911            $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">';
7091d05cddcSAtari911            $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> ';
7101d05cddcSAtari911            $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>';
71119378907SAtari911            $html .= '</div>';
7121d05cddcSAtari911            $html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">';
7131d05cddcSAtari911            $html .= $pastHtml;
7141d05cddcSAtari911            $html .= '</div>';
7151d05cddcSAtari911            $html .= '</div>';
7161d05cddcSAtari911        }
717e3a9f44cSAtari911
7181d05cddcSAtari911        // Add future events
7191d05cddcSAtari911        $html .= $futureHtml;
72019378907SAtari911
72119378907SAtari911        return $html;
72219378907SAtari911    }
72319378907SAtari911
7241d05cddcSAtari911    /**
7251d05cddcSAtari911     * Check for time conflicts between events
7261d05cddcSAtari911     */
7271d05cddcSAtari911    private function checkTimeConflicts($events) {
7281d05cddcSAtari911        // Group events by date
7291d05cddcSAtari911        $eventsByDate = [];
7301d05cddcSAtari911        foreach ($events as $date => $dateEvents) {
7311d05cddcSAtari911            if (!is_array($dateEvents)) continue;
7321d05cddcSAtari911
7331d05cddcSAtari911            foreach ($dateEvents as $evt) {
7341d05cddcSAtari911                if (empty($evt['time'])) continue; // Skip all-day events
7351d05cddcSAtari911
7361d05cddcSAtari911                if (!isset($eventsByDate[$date])) {
7371d05cddcSAtari911                    $eventsByDate[$date] = [];
7381d05cddcSAtari911                }
7391d05cddcSAtari911                $eventsByDate[$date][] = $evt;
7401d05cddcSAtari911            }
7411d05cddcSAtari911        }
7421d05cddcSAtari911
7431d05cddcSAtari911        // Check for overlaps on each date
7441d05cddcSAtari911        foreach ($eventsByDate as $date => $dateEvents) {
7451d05cddcSAtari911            for ($i = 0; $i < count($dateEvents); $i++) {
7461d05cddcSAtari911                for ($j = $i + 1; $j < count($dateEvents); $j++) {
7471d05cddcSAtari911                    if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) {
7481d05cddcSAtari911                        // Mark both events as conflicting
7491d05cddcSAtari911                        $dateEvents[$i]['hasConflict'] = true;
7501d05cddcSAtari911                        $dateEvents[$j]['hasConflict'] = true;
7511d05cddcSAtari911
7521d05cddcSAtari911                        // Store conflict info
7531d05cddcSAtari911                        if (!isset($dateEvents[$i]['conflictsWith'])) {
7541d05cddcSAtari911                            $dateEvents[$i]['conflictsWith'] = [];
7551d05cddcSAtari911                        }
7561d05cddcSAtari911                        if (!isset($dateEvents[$j]['conflictsWith'])) {
7571d05cddcSAtari911                            $dateEvents[$j]['conflictsWith'] = [];
7581d05cddcSAtari911                        }
7591d05cddcSAtari911
7601d05cddcSAtari911                        $dateEvents[$i]['conflictsWith'][] = [
7611d05cddcSAtari911                            'id' => $dateEvents[$j]['id'],
7621d05cddcSAtari911                            'title' => $dateEvents[$j]['title'],
7631d05cddcSAtari911                            'time' => $dateEvents[$j]['time'],
7641d05cddcSAtari911                            'endTime' => isset($dateEvents[$j]['endTime']) ? $dateEvents[$j]['endTime'] : ''
7651d05cddcSAtari911                        ];
7661d05cddcSAtari911
7671d05cddcSAtari911                        $dateEvents[$j]['conflictsWith'][] = [
7681d05cddcSAtari911                            'id' => $dateEvents[$i]['id'],
7691d05cddcSAtari911                            'title' => $dateEvents[$i]['title'],
7701d05cddcSAtari911                            'time' => $dateEvents[$i]['time'],
7711d05cddcSAtari911                            'endTime' => isset($dateEvents[$i]['endTime']) ? $dateEvents[$i]['endTime'] : ''
7721d05cddcSAtari911                        ];
7731d05cddcSAtari911                    }
7741d05cddcSAtari911                }
7751d05cddcSAtari911            }
7761d05cddcSAtari911
7771d05cddcSAtari911            // Update the events array with conflict information
7781d05cddcSAtari911            foreach ($events[$date] as &$evt) {
7791d05cddcSAtari911                foreach ($dateEvents as $checkedEvt) {
7801d05cddcSAtari911                    if ($evt['id'] === $checkedEvt['id']) {
7811d05cddcSAtari911                        if (isset($checkedEvt['hasConflict'])) {
7821d05cddcSAtari911                            $evt['hasConflict'] = $checkedEvt['hasConflict'];
7831d05cddcSAtari911                        }
7841d05cddcSAtari911                        if (isset($checkedEvt['conflictsWith'])) {
7851d05cddcSAtari911                            $evt['conflictsWith'] = $checkedEvt['conflictsWith'];
7861d05cddcSAtari911                        }
7871d05cddcSAtari911                        break;
7881d05cddcSAtari911                    }
7891d05cddcSAtari911                }
7901d05cddcSAtari911            }
7911d05cddcSAtari911        }
7921d05cddcSAtari911
7931d05cddcSAtari911        return $events;
7941d05cddcSAtari911    }
7951d05cddcSAtari911
7961d05cddcSAtari911    /**
7971d05cddcSAtari911     * Check if two events overlap in time
7981d05cddcSAtari911     */
7991d05cddcSAtari911    private function eventsOverlap($evt1, $evt2) {
8001d05cddcSAtari911        if (empty($evt1['time']) || empty($evt2['time'])) {
8011d05cddcSAtari911            return false; // All-day events don't conflict
8021d05cddcSAtari911        }
8031d05cddcSAtari911
8041d05cddcSAtari911        $start1 = $evt1['time'];
8051d05cddcSAtari911        $end1 = isset($evt1['endTime']) && !empty($evt1['endTime']) ? $evt1['endTime'] : $evt1['time'];
8061d05cddcSAtari911
8071d05cddcSAtari911        $start2 = $evt2['time'];
8081d05cddcSAtari911        $end2 = isset($evt2['endTime']) && !empty($evt2['endTime']) ? $evt2['endTime'] : $evt2['time'];
8091d05cddcSAtari911
8101d05cddcSAtari911        // Convert to minutes for easier comparison
8111d05cddcSAtari911        $start1Mins = $this->timeToMinutes($start1);
8121d05cddcSAtari911        $end1Mins = $this->timeToMinutes($end1);
8131d05cddcSAtari911        $start2Mins = $this->timeToMinutes($start2);
8141d05cddcSAtari911        $end2Mins = $this->timeToMinutes($end2);
8151d05cddcSAtari911
8161d05cddcSAtari911        // Check for overlap: start1 < end2 AND start2 < end1
8171d05cddcSAtari911        return $start1Mins < $end2Mins && $start2Mins < $end1Mins;
8181d05cddcSAtari911    }
8191d05cddcSAtari911
8201d05cddcSAtari911    /**
8211d05cddcSAtari911     * Convert HH:MM time to minutes since midnight
8221d05cddcSAtari911     */
8231d05cddcSAtari911    private function timeToMinutes($timeStr) {
8241d05cddcSAtari911        $parts = explode(':', $timeStr);
8251d05cddcSAtari911        if (count($parts) !== 2) return 0;
8261d05cddcSAtari911
8271d05cddcSAtari911        return (int)$parts[0] * 60 + (int)$parts[1];
8281d05cddcSAtari911    }
8291d05cddcSAtari911
83019378907SAtari911    private function renderEventPanelOnly($data) {
83119378907SAtari911        $year = (int)$data['year'];
83219378907SAtari911        $month = (int)$data['month'];
83319378907SAtari911        $namespace = $data['namespace'];
83487ac9bf3SAtari911        $height = isset($data['height']) ? $data['height'] : '400px';
83587ac9bf3SAtari911
83687ac9bf3SAtari911        // Validate height format (must be px, em, rem, vh, or %)
83787ac9bf3SAtari911        if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) {
83887ac9bf3SAtari911            $height = '400px'; // Default fallback
83987ac9bf3SAtari911        }
84019378907SAtari911
8410c3b6e81SAtari911        // Get theme - prefer inline theme= parameter, fall back to admin default
8420c3b6e81SAtari911        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();        $themeStyles = $this->getSidebarThemeStyles($theme);
8439ccd446eSAtari911
844e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
845e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
846e3a9f44cSAtari911
847e3a9f44cSAtari911        if ($isMultiNamespace) {
848e3a9f44cSAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
849e3a9f44cSAtari911        } else {
85019378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
851e3a9f44cSAtari911        }
85219378907SAtari911        $calId = 'panel_' . md5(serialize($data) . microtime());
85319378907SAtari911
85419378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
85519378907SAtari911
85619378907SAtari911        $prevMonth = $month - 1;
85719378907SAtari911        $prevYear = $year;
85819378907SAtari911        if ($prevMonth < 1) {
85919378907SAtari911            $prevMonth = 12;
86019378907SAtari911            $prevYear--;
86119378907SAtari911        }
86219378907SAtari911
86319378907SAtari911        $nextMonth = $month + 1;
86419378907SAtari911        $nextYear = $year;
86519378907SAtari911        if ($nextMonth > 12) {
86619378907SAtari911            $nextMonth = 1;
86719378907SAtari911            $nextYear++;
86819378907SAtari911        }
86919378907SAtari911
8709ccd446eSAtari911        // Determine button text color based on theme
8719ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
8729ccd446eSAtari911
873*96df7d3eSAtari911        // Get important namespaces from config for highlighting
874*96df7d3eSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
875*96df7d3eSAtari911        $importantNsList = ['important']; // default
876*96df7d3eSAtari911        if (file_exists($configFile)) {
877*96df7d3eSAtari911            $config = include $configFile;
878*96df7d3eSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
879*96df7d3eSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
880*96df7d3eSAtari911            }
881*96df7d3eSAtari911        }
882*96df7d3eSAtari911
883*96df7d3eSAtari911        $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)) . '" data-important-namespaces="' . htmlspecialchars(json_encode($importantNsList)) . '">';
8849ccd446eSAtari911
8859ccd446eSAtari911        // Inject CSS variables for this panel instance - same as main calendar
8869ccd446eSAtari911        $html .= '<style>
8879ccd446eSAtari911        #' . $calId . ' {
8889ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
8899ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
8909ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
8919ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
8929ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
8939ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
8949ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
8959ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
8969ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
8979ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
8989ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
8999ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
9009ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
9019ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
9029ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
9037e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
9047e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
9057e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
9067e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
9077e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
9087e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
9097e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
9109ccd446eSAtari911        }
9119ccd446eSAtari911        #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
9129ccd446eSAtari911        </style>';
91319378907SAtari911
9141d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
9151d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
9161d05cddcSAtari911
9171d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
9181d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
9191d05cddcSAtari911
9201d05cddcSAtari911        // Compact two-row header designed for ~500px width
9211d05cddcSAtari911        $html .= '<div class="panel-header-compact">';
9221d05cddcSAtari911
9231d05cddcSAtari911        // Row 1: Navigation and title
9241d05cddcSAtari911        $html .= '<div class="panel-header-row-1">';
9251d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
9261d05cddcSAtari911
9271d05cddcSAtari911        // Compact month name (e.g. "Feb 2026" instead of "February 2026 Events")
9281d05cddcSAtari911        $shortMonthName = date('M Y', mktime(0, 0, 0, $month, 1, $year));
9291d05cddcSAtari911        $html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $shortMonthName . '</h3>';
9301d05cddcSAtari911
9311d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
9321d05cddcSAtari911
9331d05cddcSAtari911        // Namespace badge (if applicable)
93487ac9bf3SAtari911        if ($namespace) {
935e3a9f44cSAtari911            if ($isMultiNamespace) {
936e3a9f44cSAtari911                if (strpos($namespace, '*') !== false) {
9377e8ea635SAtari911                    $html .= '<span class="panel-ns-badge" style="background:var(--cell-today-bg) !important; color:var(--text-bright) !important; -webkit-text-fill-color:var(--text-bright) !important;" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>';
938e3a9f44cSAtari911                } else {
939e3a9f44cSAtari911                    $namespaceList = array_map('trim', explode(';', $namespace));
9401d05cddcSAtari911                    $nsCount = count($namespaceList);
9417e8ea635SAtari911                    $html .= '<span class="panel-ns-badge" style="background:var(--cell-today-bg) !important; color:var(--text-bright) !important; -webkit-text-fill-color:var(--text-bright) !important;" title="' . htmlspecialchars(implode(', ', $namespaceList)) . '">' . $nsCount . ' NS</span>';
942e3a9f44cSAtari911                }
943e3a9f44cSAtari911            } else {
9441d05cddcSAtari911                $isFiltering = ($namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false);
9451d05cddcSAtari911                if ($isFiltering) {
9467e8ea635SAtari911                    $html .= '<span class="panel-ns-badge filter-on" style="background:var(--text-bright) !important; color:var(--background-site) !important; -webkit-text-fill-color:var(--background-site) !important;" title="Filtering by ' . htmlspecialchars($namespace) . ' - click to clear" onclick="clearNamespaceFilterPanel(\'' . $calId . '\')">' . htmlspecialchars($namespace) . ' ✕</span>';
9471d05cddcSAtari911                } else {
9487e8ea635SAtari911                    $html .= '<span class="panel-ns-badge" style="background:var(--cell-today-bg) !important; color:var(--text-bright) !important; -webkit-text-fill-color:var(--text-bright) !important;" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>';
94987ac9bf3SAtari911                }
950e3a9f44cSAtari911            }
9511d05cddcSAtari911        }
9521d05cddcSAtari911
9531d05cddcSAtari911        $html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
95419378907SAtari911        $html .= '</div>';
95519378907SAtari911
9561d05cddcSAtari911        // Row 2: Search and add button
9571d05cddcSAtari911        $html .= '<div class="panel-header-row-2">';
9581d05cddcSAtari911        $html .= '<div class="panel-search-box">';
959*96df7d3eSAtari911        $html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placeholder="Search this month..." oninput="filterEvents(\'' . $calId . '\', this.value)">';
9601d05cddcSAtari911        $html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
961*96df7d3eSAtari911        $html .= '<button class="panel-search-mode" id="search-mode-' . $calId . '" onclick="toggleSearchMode(\'' . $calId . '\', \'' . $namespace . '\')" title="Search this month only">��</button>';
9621d05cddcSAtari911        $html .= '</div>';
9631d05cddcSAtari911        $html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
9641d05cddcSAtari911        $html .= '</div>';
9651d05cddcSAtari911
96619378907SAtari911        $html .= '</div>';
96719378907SAtari911
96887ac9bf3SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">';
96919378907SAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace);
97019378907SAtari911        $html .= '</div>';
97119378907SAtari911
9720c3b6e81SAtari911        $html .= $this->renderEventDialog($calId, $namespace, $theme);
97319378907SAtari911
97487ac9bf3SAtari911        // Month/Year picker for event panel
9759ccd446eSAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
97687ac9bf3SAtari911
97719378907SAtari911        $html .= '</div>';
97819378907SAtari911
97919378907SAtari911        return $html;
98019378907SAtari911    }
98119378907SAtari911
98219378907SAtari911    private function renderStandaloneEventList($data) {
98319378907SAtari911        $namespace = $data['namespace'];
9841d05cddcSAtari911        // If no namespace specified, show all namespaces
9851d05cddcSAtari911        if (empty($namespace)) {
9861d05cddcSAtari911            $namespace = '*';
9871d05cddcSAtari911        }
98819378907SAtari911        $daterange = $data['daterange'];
98919378907SAtari911        $date = $data['date'];
990e3a9f44cSAtari911        $range = isset($data['range']) ? strtolower($data['range']) : '';
99187ac9bf3SAtari911        $today = isset($data['today']) ? true : false;
992e3a9f44cSAtari911        $sidebar = isset($data['sidebar']) ? true : false;
9931d05cddcSAtari911        $showchecked = isset($data['showchecked']) ? true : false; // New parameter
9941d05cddcSAtari911        $noheader = isset($data['noheader']) ? true : false; // New parameter to hide header
99519378907SAtari911
996e3a9f44cSAtari911        // Handle "range" parameter - day, week, or month
997e3a9f44cSAtari911        if ($range === 'day') {
9981d05cddcSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
99987ac9bf3SAtari911            $endDate = date('Y-m-d');
1000e3a9f44cSAtari911            $headerText = 'Today';
1001e3a9f44cSAtari911        } elseif ($range === 'week') {
10021d05cddcSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
10031d05cddcSAtari911            $endDateTime = new DateTime();
1004e3a9f44cSAtari911            $endDateTime->modify('+7 days');
1005e3a9f44cSAtari911            $endDate = $endDateTime->format('Y-m-d');
1006e3a9f44cSAtari911            $headerText = 'This Week';
1007e3a9f44cSAtari911        } elseif ($range === 'month') {
10081d05cddcSAtari911            $startDate = date('Y-m-01', strtotime('-1 month')); // Include previous month for past due tasks
1009e3a9f44cSAtari911            $endDate = date('Y-m-t'); // Last of current month
10101d05cddcSAtari911            $dt = new DateTime();
1011e3a9f44cSAtari911            $headerText = $dt->format('F Y');
1012e3a9f44cSAtari911        } elseif ($sidebar) {
10131d05cddcSAtari911            // NEW: Sidebar widget - load current week's events
10149ccd446eSAtari911            $weekStartDay = $this->getWeekStartDay(); // Get saved preference
10159ccd446eSAtari911
10169ccd446eSAtari911            if ($weekStartDay === 'monday') {
10179ccd446eSAtari911                // Monday start
10181d05cddcSAtari911                $weekStart = date('Y-m-d', strtotime('monday this week'));
10191d05cddcSAtari911                $weekEnd = date('Y-m-d', strtotime('sunday this week'));
10209ccd446eSAtari911            } else {
10219ccd446eSAtari911                // Sunday start (default - US/Canada standard)
10229ccd446eSAtari911                $today = date('w'); // 0 (Sun) to 6 (Sat)
10239ccd446eSAtari911                if ($today == 0) {
10249ccd446eSAtari911                    // Today is Sunday
10259ccd446eSAtari911                    $weekStart = date('Y-m-d');
10269ccd446eSAtari911                } else {
10279ccd446eSAtari911                    // Monday-Saturday: go back to last Sunday
10289ccd446eSAtari911                    $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
10299ccd446eSAtari911                }
10309ccd446eSAtari911                $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
10319ccd446eSAtari911            }
10321d05cddcSAtari911
10339ccd446eSAtari911            // Load events for the entire week PLUS tomorrow (if tomorrow is outside week)
10349ccd446eSAtari911            // PLUS next 2 weeks for Important events
10351d05cddcSAtari911            $start = new DateTime($weekStart);
10361d05cddcSAtari911            $end = new DateTime($weekEnd);
10379ccd446eSAtari911
10389ccd446eSAtari911            // Check if we need to extend to include tomorrow
10399ccd446eSAtari911            $tomorrowDate = date('Y-m-d', strtotime('+1 day'));
10409ccd446eSAtari911            if ($tomorrowDate > $weekEnd) {
10419ccd446eSAtari911                // Tomorrow is outside the week, extend end date to include it
10429ccd446eSAtari911                $end = new DateTime($tomorrowDate);
10439ccd446eSAtari911            }
10449ccd446eSAtari911
10459ccd446eSAtari911            // Extend 2 weeks into the future for Important events
10469ccd446eSAtari911            $twoWeeksOut = date('Y-m-d', strtotime($weekEnd . ' +14 days'));
10479ccd446eSAtari911            $end = new DateTime($twoWeeksOut);
10489ccd446eSAtari911
10491d05cddcSAtari911            $end->modify('+1 day'); // DatePeriod excludes end date
10501d05cddcSAtari911            $interval = new DateInterval('P1D');
10511d05cddcSAtari911            $period = new DatePeriod($start, $interval, $end);
10521d05cddcSAtari911
10531d05cddcSAtari911            $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
10541d05cddcSAtari911            $allEvents = [];
10551d05cddcSAtari911            $loadedMonths = [];
10561d05cddcSAtari911
10571d05cddcSAtari911            foreach ($period as $dt) {
10581d05cddcSAtari911                $year = (int)$dt->format('Y');
10591d05cddcSAtari911                $month = (int)$dt->format('n');
10601d05cddcSAtari911                $dateKey = $dt->format('Y-m-d');
10611d05cddcSAtari911
10621d05cddcSAtari911                $monthKey = $year . '-' . $month . '-' . $namespace;
10631d05cddcSAtari911
10641d05cddcSAtari911                if (!isset($loadedMonths[$monthKey])) {
10651d05cddcSAtari911                    if ($isMultiNamespace) {
10661d05cddcSAtari911                        $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
10671d05cddcSAtari911                    } else {
10681d05cddcSAtari911                        $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
10691d05cddcSAtari911                    }
10701d05cddcSAtari911                }
10711d05cddcSAtari911
10721d05cddcSAtari911                $monthEvents = $loadedMonths[$monthKey];
10731d05cddcSAtari911
10741d05cddcSAtari911                if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
10751d05cddcSAtari911                    $allEvents[$dateKey] = $monthEvents[$dateKey];
10761d05cddcSAtari911                }
10771d05cddcSAtari911            }
10781d05cddcSAtari911
10791d05cddcSAtari911            // Apply time conflict detection
10801d05cddcSAtari911            $allEvents = $this->checkTimeConflicts($allEvents);
10811d05cddcSAtari911
10821d05cddcSAtari911            $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8);
10831d05cddcSAtari911
10841d05cddcSAtari911            // Render sidebar widget and return immediately
10850c3b6e81SAtari911            $themeOverride = !empty($data['theme']) ? $data['theme'] : null;
10860c3b6e81SAtari911            return $this->renderSidebarWidget($allEvents, $namespace, $calId, $themeOverride);
1087e3a9f44cSAtari911        } elseif ($today) {
1088e3a9f44cSAtari911            $startDate = date('Y-m-d');
1089e3a9f44cSAtari911            $endDate = date('Y-m-d');
1090e3a9f44cSAtari911            $headerText = 'Today';
109187ac9bf3SAtari911        } elseif ($daterange) {
109219378907SAtari911            list($startDate, $endDate) = explode(':', $daterange);
1093e3a9f44cSAtari911            $start = new DateTime($startDate);
1094e3a9f44cSAtari911            $end = new DateTime($endDate);
1095e3a9f44cSAtari911            $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y');
109619378907SAtari911        } elseif ($date) {
109719378907SAtari911            $startDate = $date;
109819378907SAtari911            $endDate = $date;
1099e3a9f44cSAtari911            $dt = new DateTime($date);
1100e3a9f44cSAtari911            $headerText = $dt->format('l, F j, Y');
110119378907SAtari911        } else {
110219378907SAtari911            $startDate = date('Y-m-01');
110319378907SAtari911            $endDate = date('Y-m-t');
1104e3a9f44cSAtari911            $dt = new DateTime($startDate);
1105e3a9f44cSAtari911            $headerText = $dt->format('F Y');
110619378907SAtari911        }
110719378907SAtari911
1108e3a9f44cSAtari911        // Load all events in date range
110919378907SAtari911        $allEvents = array();
111019378907SAtari911        $start = new DateTime($startDate);
111119378907SAtari911        $end = new DateTime($endDate);
111219378907SAtari911        $end->modify('+1 day');
111319378907SAtari911
111419378907SAtari911        $interval = new DateInterval('P1D');
111519378907SAtari911        $period = new DatePeriod($start, $interval, $end);
111619378907SAtari911
1117e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
1118e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
1119e3a9f44cSAtari911
112019378907SAtari911        static $loadedMonths = array();
112119378907SAtari911
112219378907SAtari911        foreach ($period as $dt) {
112319378907SAtari911            $year = (int)$dt->format('Y');
112419378907SAtari911            $month = (int)$dt->format('n');
112519378907SAtari911            $dateKey = $dt->format('Y-m-d');
112619378907SAtari911
1127e3a9f44cSAtari911            $monthKey = $year . '-' . $month . '-' . $namespace;
112819378907SAtari911
112919378907SAtari911            if (!isset($loadedMonths[$monthKey])) {
1130e3a9f44cSAtari911                if ($isMultiNamespace) {
1131e3a9f44cSAtari911                    $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
1132e3a9f44cSAtari911                } else {
113319378907SAtari911                    $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
113419378907SAtari911                }
1135e3a9f44cSAtari911            }
113619378907SAtari911
113719378907SAtari911            $monthEvents = $loadedMonths[$monthKey];
113819378907SAtari911
113919378907SAtari911            if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
114019378907SAtari911                $allEvents[$dateKey] = $monthEvents[$dateKey];
114119378907SAtari911            }
114219378907SAtari911        }
114319378907SAtari911
11441d05cddcSAtari911        // Sort events by date (already sorted by dateKey), then by time within each day
11451d05cddcSAtari911        foreach ($allEvents as $dateKey => &$dayEvents) {
11461d05cddcSAtari911            usort($dayEvents, function($a, $b) {
11471d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
11481d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
11491d05cddcSAtari911
11501d05cddcSAtari911                // All-day events (no time) go to the TOP
11511d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
11521d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
11531d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
11541d05cddcSAtari911
11551d05cddcSAtari911                // Both have times, sort chronologically
11561d05cddcSAtari911                return strcmp($timeA, $timeB);
11571d05cddcSAtari911            });
11581d05cddcSAtari911        }
11591d05cddcSAtari911        unset($dayEvents); // Break reference
11601d05cddcSAtari911
1161e3a9f44cSAtari911        // Simple 2-line display widget
11621d05cddcSAtari911        $calId = 'eventlist_' . uniqid();
11637e8ea635SAtari911        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();
11647e8ea635SAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
11657e8ea635SAtari911        $isDark = in_array($theme, ['matrix', 'purple', 'pink']);
11667e8ea635SAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
11677e8ea635SAtari911
11687e8ea635SAtari911        // Theme class for CSS targeting
11697e8ea635SAtari911        $themeClass = 'eventlist-theme-' . $theme;
11707e8ea635SAtari911
11717e8ea635SAtari911        // Container styling - dark themes get border + glow, light themes get subtle border
11727e8ea635SAtari911        $containerStyle = 'background:' . $themeStyles['bg'] . ' !important;';
11737e8ea635SAtari911        if ($isDark) {
11747e8ea635SAtari911            $containerStyle .= ' border:2px solid ' . $themeStyles['border'] . ';';
11757e8ea635SAtari911            $containerStyle .= ' border-radius:4px;';
11767e8ea635SAtari911            $containerStyle .= ' box-shadow:0 0 10px ' . $themeStyles['shadow'] . ';';
11777e8ea635SAtari911        } else {
11787e8ea635SAtari911            $containerStyle .= ' border:1px solid ' . $themeStyles['grid_border'] . ';';
11797e8ea635SAtari911            $containerStyle .= ' border-radius:4px;';
11807e8ea635SAtari911        }
11817e8ea635SAtari911
11827e8ea635SAtari911        $html = '<div class="eventlist-simple ' . $themeClass . '" id="' . $calId . '" style="' . $containerStyle . '">';
11837e8ea635SAtari911
11847e8ea635SAtari911        // Inject CSS variables for this eventlist instance
11857e8ea635SAtari911        $html .= '<style>
11867e8ea635SAtari911        #' . $calId . ' {
11877e8ea635SAtari911            --background-site: ' . $themeStyles['bg'] . ';
11887e8ea635SAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
11897e8ea635SAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
11907e8ea635SAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
11917e8ea635SAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
11927e8ea635SAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
11937e8ea635SAtari911            --border-main: ' . $themeStyles['border'] . ';
11947e8ea635SAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
11957e8ea635SAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
11967e8ea635SAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
11977e8ea635SAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
11987e8ea635SAtari911            --btn-text: ' . $btnTextColor . ';
11997e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
12007e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
12017e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
12027e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
12037e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
12047e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
12057e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
12067e8ea635SAtari911        }
12077e8ea635SAtari911        </style>';
12081d05cddcSAtari911
12091d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
12101d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
12111d05cddcSAtari911
12121d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
12131d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
12141d05cddcSAtari911
12151d05cddcSAtari911        // Add compact header with date and clock for "today" mode (unless noheader is set)
12161d05cddcSAtari911        if ($today && !empty($allEvents) && !$noheader) {
12171d05cddcSAtari911            $todayDate = new DateTime();
12181d05cddcSAtari911            $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026"
12191d05cddcSAtari911            $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM"
12201d05cddcSAtari911
12211d05cddcSAtari911            $html .= '<div class="eventlist-today-header">';
12221d05cddcSAtari911            $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>';
12231d05cddcSAtari911            $html .= '<div class="eventlist-bottom-info">';
12241d05cddcSAtari911            $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '">--°</span></span>';
12251d05cddcSAtari911            $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>';
12261d05cddcSAtari911            $html .= '</div>';
12271d05cddcSAtari911
12281d05cddcSAtari911            // Three CPU/Memory bars (all update live)
12291d05cddcSAtari911            $html .= '<div class="eventlist-stats-container">';
12301d05cddcSAtari911
12311d05cddcSAtari911            // 5-minute load average (green, updates every 2 seconds)
12327e8ea635SAtari911            $html .= '<div class="eventlist-cpu-bar" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $calId . '(\'green\')" onmouseout="hideTooltip_' . $calId . '(\'green\')">';
12337e8ea635SAtari911            $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_bright'] . ' !important;"></div>';
12341d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
12351d05cddcSAtari911            $html .= '</div>';
12361d05cddcSAtari911
12371d05cddcSAtari911            // Real-time CPU (purple, updates with 5-sec average)
12387e8ea635SAtari911            $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $calId . '(\'purple\')" onmouseout="hideTooltip_' . $calId . '(\'purple\')">';
12397e8ea635SAtari911            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['border'] . ' !important;"></div>';
12401d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
12411d05cddcSAtari911            $html .= '</div>';
12421d05cddcSAtari911
12431d05cddcSAtari911            // Real-time Memory (orange, updates)
12447e8ea635SAtari911            $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $calId . '(\'orange\')" onmouseout="hideTooltip_' . $calId . '(\'orange\')">';
12457e8ea635SAtari911            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_primary'] . ' !important;"></div>';
12461d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
12471d05cddcSAtari911            $html .= '</div>';
12481d05cddcSAtari911
12491d05cddcSAtari911            $html .= '</div>';
12501d05cddcSAtari911            $html .= '</div>';
12511d05cddcSAtari911
12521d05cddcSAtari911            // Add JavaScript to update clock and weather
12531d05cddcSAtari911            $html .= '<script>
12541d05cddcSAtari911(function() {
12551d05cddcSAtari911    // Update clock every second
12561d05cddcSAtari911    function updateClock() {
12571d05cddcSAtari911        const now = new Date();
12581d05cddcSAtari911        let hours = now.getHours();
12591d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
12601d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
12611d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
12621d05cddcSAtari911        hours = hours % 12 || 12;
12631d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
12641d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
12651d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
12661d05cddcSAtari911    }
12671d05cddcSAtari911    setInterval(updateClock, 1000);
12681d05cddcSAtari911
1269*96df7d3eSAtari911    // Fetch weather - uses default location, click weather to get local
1270*96df7d3eSAtari911    var userLocationGranted = false;
1271*96df7d3eSAtari911    var userLat = 38.5816;  // Sacramento default
1272*96df7d3eSAtari911    var userLon = -121.4944;
12731d05cddcSAtari911
1274*96df7d3eSAtari911    function fetchWeatherData(lat, lon) {
1275*96df7d3eSAtari911        fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_weather=true&temperature_unit=fahrenheit")
12761d05cddcSAtari911            .then(response => response.json())
12771d05cddcSAtari911            .then(data => {
12781d05cddcSAtari911                if (data.current_weather) {
12791d05cddcSAtari911                    const temp = Math.round(data.current_weather.temperature);
12801d05cddcSAtari911                    const weatherCode = data.current_weather.weathercode;
12811d05cddcSAtari911                    const icon = getWeatherIcon(weatherCode);
12821d05cddcSAtari911                    const iconEl = document.getElementById("weather-icon-' . $calId . '");
12831d05cddcSAtari911                    const tempEl = document.getElementById("weather-temp-' . $calId . '");
12841d05cddcSAtari911                    if (iconEl) iconEl.textContent = icon;
12851d05cddcSAtari911                    if (tempEl) tempEl.innerHTML = temp + "&deg;";
12861d05cddcSAtari911                }
12871d05cddcSAtari911            })
12881d05cddcSAtari911            .catch(error => {
12891d05cddcSAtari911                console.log("Weather fetch error:", error);
12901d05cddcSAtari911            });
1291*96df7d3eSAtari911    }
1292*96df7d3eSAtari911
1293*96df7d3eSAtari911    function updateWeather() {
1294*96df7d3eSAtari911        fetchWeatherData(userLat, userLon);
1295*96df7d3eSAtari911    }
1296*96df7d3eSAtari911
1297*96df7d3eSAtari911    // Allow user to click weather to get local weather (requires user gesture)
1298*96df7d3eSAtari911    function requestLocalWeather() {
1299*96df7d3eSAtari911        if (userLocationGranted) return; // Already have permission
1300*96df7d3eSAtari911
1301*96df7d3eSAtari911        if ("geolocation" in navigator) {
1302*96df7d3eSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
1303*96df7d3eSAtari911                userLat = position.coords.latitude;
1304*96df7d3eSAtari911                userLon = position.coords.longitude;
1305*96df7d3eSAtari911                userLocationGranted = true;
1306*96df7d3eSAtari911                fetchWeatherData(userLat, userLon);
13071d05cddcSAtari911            }, function(error) {
1308*96df7d3eSAtari911                console.log("Geolocation denied or unavailable, using default location");
13091d05cddcSAtari911            });
13101d05cddcSAtari911        }
13111d05cddcSAtari911    }
13121d05cddcSAtari911
1313*96df7d3eSAtari911    // Add click handler to weather widget for local weather
1314*96df7d3eSAtari911    setTimeout(function() {
1315*96df7d3eSAtari911        var weatherEl = document.querySelector("#weather-icon-' . $calId . '");
1316*96df7d3eSAtari911        if (weatherEl) {
1317*96df7d3eSAtari911            weatherEl.style.cursor = "pointer";
1318*96df7d3eSAtari911            weatherEl.title = "Click for local weather";
1319*96df7d3eSAtari911            weatherEl.addEventListener("click", requestLocalWeather);
1320*96df7d3eSAtari911        }
1321*96df7d3eSAtari911    }, 100);
1322*96df7d3eSAtari911
13231d05cddcSAtari911    // WMO Weather interpretation codes
13241d05cddcSAtari911    function getWeatherIcon(code) {
13251d05cddcSAtari911        const icons = {
13261d05cddcSAtari911            0: "☀️",   // Clear sky
13271d05cddcSAtari911            1: "��️",   // Mainly clear
13281d05cddcSAtari911            2: "⛅",   // Partly cloudy
13291d05cddcSAtari911            3: "☁️",   // Overcast
13301d05cddcSAtari911            45: "��️",  // Fog
13311d05cddcSAtari911            48: "��️",  // Depositing rime fog
13321d05cddcSAtari911            51: "��️",  // Light drizzle
13331d05cddcSAtari911            53: "��️",  // Moderate drizzle
13341d05cddcSAtari911            55: "��️",  // Dense drizzle
13351d05cddcSAtari911            61: "��️",  // Slight rain
13361d05cddcSAtari911            63: "��️",  // Moderate rain
13371d05cddcSAtari911            65: "⛈️",  // Heavy rain
13381d05cddcSAtari911            71: "��️",  // Slight snow
13391d05cddcSAtari911            73: "��️",  // Moderate snow
13401d05cddcSAtari911            75: "❄️",  // Heavy snow
13411d05cddcSAtari911            77: "��️",  // Snow grains
13421d05cddcSAtari911            80: "��️",  // Slight rain showers
13431d05cddcSAtari911            81: "��️",  // Moderate rain showers
13441d05cddcSAtari911            82: "⛈️",  // Violent rain showers
13451d05cddcSAtari911            85: "��️",  // Slight snow showers
13461d05cddcSAtari911            86: "❄️",  // Heavy snow showers
13471d05cddcSAtari911            95: "⛈️",  // Thunderstorm
13481d05cddcSAtari911            96: "⛈️",  // Thunderstorm with slight hail
13491d05cddcSAtari911            99: "⛈️"   // Thunderstorm with heavy hail
13501d05cddcSAtari911        };
13511d05cddcSAtari911        return icons[code] || "��️";
13521d05cddcSAtari911    }
13531d05cddcSAtari911
13541d05cddcSAtari911    // Update weather immediately and every 10 minutes
13551d05cddcSAtari911    updateWeather();
13561d05cddcSAtari911    setInterval(updateWeather, 600000);
13571d05cddcSAtari911
13581d05cddcSAtari911    // CPU load history for 4-second rolling average
13591d05cddcSAtari911    const cpuHistory = [];
13601d05cddcSAtari911    const CPU_HISTORY_SIZE = 2; // 2 samples × 2 seconds = 4 seconds
13611d05cddcSAtari911
13621d05cddcSAtari911    // Store latest system stats for tooltips
13631d05cddcSAtari911    let latestStats = {
13641d05cddcSAtari911        load: {"1min": 0, "5min": 0, "15min": 0},
13651d05cddcSAtari911        uptime: "",
13661d05cddcSAtari911        memory_details: {},
13671d05cddcSAtari911        top_processes: []
13681d05cddcSAtari911    };
13691d05cddcSAtari911
13701d05cddcSAtari911    // Tooltip functions
13711d05cddcSAtari911    window["showTooltip_' . $calId . '"] = function(color) {
13721d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
13731d05cddcSAtari911        if (!tooltip) {
13741d05cddcSAtari911            console.log("Tooltip element not found for color:", color);
13751d05cddcSAtari911            return;
13761d05cddcSAtari911        }
13771d05cddcSAtari911
13781d05cddcSAtari911
13791d05cddcSAtari911        let content = "";
13801d05cddcSAtari911
13811d05cddcSAtari911        if (color === "green") {
13821d05cddcSAtari911            // Green bar: Load averages and uptime
13831d05cddcSAtari911            content = "<div class=\"tooltip-title\">CPU Load Average</div>";
13841d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
13851d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
13861d05cddcSAtari911            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
13871d05cddcSAtari911            if (latestStats.uptime) {
13887e8ea635SAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_bright'] . ';\">Uptime: " + latestStats.uptime + "</div>";
13891d05cddcSAtari911            }
13907e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['text_bright'] . '", "important");
13917e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['text_bright'] . '", "important");
13927e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_bright'] . '", "important");
13931d05cddcSAtari911        } else if (color === "purple") {
13941d05cddcSAtari911            // Purple bar: Load averages (short-term) and top processes
13951d05cddcSAtari911            content = "<div class=\"tooltip-title\">CPU Load (Short-term)</div>";
13961d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
13971d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
13981d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
13997e8ea635SAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['border'] . ';\" class=\"tooltip-title\">Top Processes</div>";
14001d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
14011d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
14021d05cddcSAtari911                });
14031d05cddcSAtari911            }
14047e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['border'] . '", "important");
14057e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['border'] . '", "important");
14067e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['border'] . '", "important");
14071d05cddcSAtari911        } else if (color === "orange") {
14081d05cddcSAtari911            // Orange bar: Memory details and top processes
14091d05cddcSAtari911            content = "<div class=\"tooltip-title\">Memory Usage</div>";
14101d05cddcSAtari911            if (latestStats.memory_details && latestStats.memory_details.total) {
14111d05cddcSAtari911                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
14121d05cddcSAtari911                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
14131d05cddcSAtari911                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
14141d05cddcSAtari911                if (latestStats.memory_details.cached) {
14151d05cddcSAtari911                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
14161d05cddcSAtari911                }
14171d05cddcSAtari911            } else {
14181d05cddcSAtari911                content += "<div>Loading...</div>";
14191d05cddcSAtari911            }
14201d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
14217e8ea635SAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_primary'] . ';\" class=\"tooltip-title\">Top Processes</div>";
14221d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
14231d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
14241d05cddcSAtari911                });
14251d05cddcSAtari911            }
14267e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['text_primary'] . '", "important");
14277e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['text_primary'] . '", "important");
14287e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_primary'] . '", "important");
14291d05cddcSAtari911        }
14301d05cddcSAtari911
14311d05cddcSAtari911        tooltip.innerHTML = content;
14327e8ea635SAtari911        tooltip.style.setProperty("display", "block");
14337e8ea635SAtari911        tooltip.style.setProperty("background", "' . $themeStyles['bg'] . '", "important");
14341d05cddcSAtari911
14351d05cddcSAtari911        // Position tooltip using fixed positioning above the bar
14361d05cddcSAtari911        const bar = tooltip.parentElement;
14371d05cddcSAtari911        const barRect = bar.getBoundingClientRect();
14381d05cddcSAtari911        const tooltipRect = tooltip.getBoundingClientRect();
14391d05cddcSAtari911
14401d05cddcSAtari911        // Center horizontally on the bar
14411d05cddcSAtari911        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
14421d05cddcSAtari911        // Position above the bar with 8px gap
14431d05cddcSAtari911        const top = barRect.top - tooltipRect.height - 8;
14441d05cddcSAtari911
14451d05cddcSAtari911        tooltip.style.left = left + "px";
14461d05cddcSAtari911        tooltip.style.top = top + "px";
14471d05cddcSAtari911    };
14481d05cddcSAtari911
14491d05cddcSAtari911    window["hideTooltip_' . $calId . '"] = function(color) {
14501d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
14511d05cddcSAtari911        if (tooltip) {
14521d05cddcSAtari911            tooltip.style.display = "none";
14531d05cddcSAtari911        }
14541d05cddcSAtari911    };
14551d05cddcSAtari911
14561d05cddcSAtari911    // Update CPU and memory bars every 2 seconds
14571d05cddcSAtari911    function updateSystemStats() {
14581d05cddcSAtari911        // Fetch real system stats from server
14591d05cddcSAtari911        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
14601d05cddcSAtari911            .then(response => response.json())
14611d05cddcSAtari911            .then(data => {
14621d05cddcSAtari911
14631d05cddcSAtari911                // Store data for tooltips
14641d05cddcSAtari911                latestStats = {
14651d05cddcSAtari911                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
14661d05cddcSAtari911                    uptime: data.uptime || "",
14671d05cddcSAtari911                    memory_details: data.memory_details || {},
14681d05cddcSAtari911                    top_processes: data.top_processes || []
14691d05cddcSAtari911                };
14701d05cddcSAtari911
14711d05cddcSAtari911
14721d05cddcSAtari911                // Update green bar (5-minute average) - updates live now!
14731d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
14741d05cddcSAtari911                if (greenBar) {
14751d05cddcSAtari911                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
14761d05cddcSAtari911                }
14771d05cddcSAtari911
14781d05cddcSAtari911                // Add current CPU to history for purple bar
14791d05cddcSAtari911                cpuHistory.push(data.cpu);
14801d05cddcSAtari911                if (cpuHistory.length > CPU_HISTORY_SIZE) {
14811d05cddcSAtari911                    cpuHistory.shift(); // Remove oldest
14821d05cddcSAtari911                }
14831d05cddcSAtari911
14841d05cddcSAtari911                // Calculate 5-second average for CPU
14851d05cddcSAtari911                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
14861d05cddcSAtari911
14871d05cddcSAtari911                // Update CPU bar (purple) with 5-second average
14881d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
14891d05cddcSAtari911                if (cpuBar) {
14901d05cddcSAtari911                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
14911d05cddcSAtari911                }
14921d05cddcSAtari911
14931d05cddcSAtari911                // Update memory bar (orange) with real data
14941d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
14951d05cddcSAtari911                if (memBar) {
14961d05cddcSAtari911                    memBar.style.width = Math.min(100, data.memory) + "%";
14971d05cddcSAtari911                }
14981d05cddcSAtari911            })
14991d05cddcSAtari911            .catch(error => {
15001d05cddcSAtari911                console.log("System stats error:", error);
15011d05cddcSAtari911                // Fallback to client-side estimates on error
15021d05cddcSAtari911                const cpuFallback = Math.random() * 100;
15031d05cddcSAtari911                cpuHistory.push(cpuFallback);
15041d05cddcSAtari911                if (cpuHistory.length > CPU_HISTORY_SIZE) {
15051d05cddcSAtari911                    cpuHistory.shift();
15061d05cddcSAtari911                }
15071d05cddcSAtari911                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
15081d05cddcSAtari911
15091d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
15101d05cddcSAtari911                if (greenBar) greenBar.style.width = Math.min(100, cpuFallback) + "%";
15111d05cddcSAtari911
15121d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
15131d05cddcSAtari911                if (cpuBar) cpuBar.style.width = Math.min(100, cpuAverage) + "%";
15141d05cddcSAtari911
15151d05cddcSAtari911                let memoryUsage = 0;
15161d05cddcSAtari911                if (performance.memory) {
15171d05cddcSAtari911                    memoryUsage = (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100;
15181d05cddcSAtari911                } else {
15191d05cddcSAtari911                    memoryUsage = Math.random() * 100;
15201d05cddcSAtari911                }
15211d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
15221d05cddcSAtari911                if (memBar) memBar.style.width = Math.min(100, memoryUsage) + "%";
15231d05cddcSAtari911            });
15241d05cddcSAtari911    }
15251d05cddcSAtari911
15261d05cddcSAtari911    // Update immediately and then every 2 seconds
15271d05cddcSAtari911    updateSystemStats();
15281d05cddcSAtari911    setInterval(updateSystemStats, 2000);
15291d05cddcSAtari911})();
15301d05cddcSAtari911</script>';
15311d05cddcSAtari911        }
153219378907SAtari911
153319378907SAtari911        if (empty($allEvents)) {
1534e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-empty">';
1535e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText);
1536e3a9f44cSAtari911            if ($namespace) {
1537e3a9f44cSAtari911                $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>';
153887ac9bf3SAtari911            }
1539e3a9f44cSAtari911            $html .= '</div>';
1540e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-body">No events</div>';
1541e3a9f44cSAtari911            $html .= '</div>';
1542e3a9f44cSAtari911        } else {
1543e3a9f44cSAtari911            // Calculate today and tomorrow's dates for highlighting
15441d05cddcSAtari911            $todayStr = date('Y-m-d');
1545e3a9f44cSAtari911            $tomorrow = date('Y-m-d', strtotime('+1 day'));
1546e3a9f44cSAtari911
1547e3a9f44cSAtari911            foreach ($allEvents as $dateKey => $dayEvents) {
1548e3a9f44cSAtari911                $dateObj = new DateTime($dateKey);
1549e3a9f44cSAtari911                $displayDate = $dateObj->format('D, M j');
1550e3a9f44cSAtari911
15511d05cddcSAtari911                // Check if this date is today or tomorrow or past
1552e3a9f44cSAtari911                // Enable highlighting for sidebar mode AND range modes (day, week, month)
1553e3a9f44cSAtari911                $enableHighlighting = $sidebar || !empty($range);
15541d05cddcSAtari911                $isToday = $enableHighlighting && ($dateKey === $todayStr);
1555e3a9f44cSAtari911                $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow);
15561d05cddcSAtari911                $isPast = $dateKey < $todayStr;
155719378907SAtari911
155819378907SAtari911                foreach ($dayEvents as $event) {
15591d05cddcSAtari911                    // Check if this is a task and if it's completed
15601d05cddcSAtari911                    $isTask = !empty($event['isTask']);
15611d05cddcSAtari911                    $completed = !empty($event['completed']);
15621d05cddcSAtari911
15631d05cddcSAtari911                    // ALWAYS skip completed tasks UNLESS showchecked is explicitly set
15641d05cddcSAtari911                    if (!$showchecked && $isTask && $completed) {
1565e3a9f44cSAtari911                        continue;
1566e3a9f44cSAtari911                    }
156719378907SAtari911
15681d05cddcSAtari911                    // Skip past events that are NOT tasks (only show past due tasks from the past)
15691d05cddcSAtari911                    if ($isPast && !$isTask) {
15701d05cddcSAtari911                        continue;
15711d05cddcSAtari911                    }
15721d05cddcSAtari911
15731d05cddcSAtari911                    // Determine if task is past due (past date, is task, not completed)
15741d05cddcSAtari911                    $isPastDue = $isPast && $isTask && !$completed;
15751d05cddcSAtari911
1576e3a9f44cSAtari911                    // Line 1: Header (Title, Time, Date, Namespace)
1577e3a9f44cSAtari911                    $todayClass = $isToday ? ' eventlist-simple-today' : '';
1578e3a9f44cSAtari911                    $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : '';
15791d05cddcSAtari911                    $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : '';
15801d05cddcSAtari911                    $html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . $pastDueClass . '">';
1581e3a9f44cSAtari911                    $html .= '<div class="eventlist-simple-header">';
1582e3a9f44cSAtari911
1583e3a9f44cSAtari911                    // Title
1584e3a9f44cSAtari911                    $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>';
1585e3a9f44cSAtari911
1586e3a9f44cSAtari911                    // Time (12-hour format)
1587e3a9f44cSAtari911                    if (!empty($event['time'])) {
1588e3a9f44cSAtari911                        $timeParts = explode(':', $event['time']);
158987ac9bf3SAtari911                        if (count($timeParts) === 2) {
159087ac9bf3SAtari911                            $hour = (int)$timeParts[0];
159187ac9bf3SAtari911                            $minute = $timeParts[1];
159287ac9bf3SAtari911                            $ampm = $hour >= 12 ? 'PM' : 'AM';
1593e3a9f44cSAtari911                            $hour = $hour % 12 ?: 12;
159487ac9bf3SAtari911                            $displayTime = $hour . ':' . $minute . ' ' . $ampm;
1595e3a9f44cSAtari911                            $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>';
159619378907SAtari911                        }
159787ac9bf3SAtari911                    }
159887ac9bf3SAtari911
1599e3a9f44cSAtari911                    // Date
1600e3a9f44cSAtari911                    $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>';
1601e3a9f44cSAtari911
16021d05cddcSAtari911                    // Badge: PAST DUE, TODAY, or nothing
16031d05cddcSAtari911                    if ($isPastDue) {
16047e8ea635SAtari911                        $html .= ' <span class="eventlist-simple-pastdue-badge" style="background:' . $themeStyles['pastdue_color'] . ' !important; color:white !important; -webkit-text-fill-color:white !important;">PAST DUE</span>';
16051d05cddcSAtari911                    } elseif ($isToday) {
16067e8ea635SAtari911                        $html .= ' <span class="eventlist-simple-today-badge" style="background:' . $themeStyles['border'] . ' !important; color:' . $themeStyles['bg'] . ' !important; -webkit-text-fill-color:' . $themeStyles['bg'] . ' !important;">TODAY</span>';
160787ac9bf3SAtari911                    }
1608e3a9f44cSAtari911
1609e3a9f44cSAtari911                    // Namespace badge (show individual event's namespace)
1610e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
1611e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
1612e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading
161319378907SAtari911                    }
1614e3a9f44cSAtari911                    if ($eventNamespace) {
1615e3a9f44cSAtari911                        $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</span>';
1616e3a9f44cSAtari911                    }
1617e3a9f44cSAtari911
1618e3a9f44cSAtari911                    $html .= '</div>'; // header
1619e3a9f44cSAtari911
1620e3a9f44cSAtari911                    // Line 2: Body (Description only) - only show if description exists
1621e3a9f44cSAtari911                    if (!empty($event['description'])) {
1622e3a9f44cSAtari911                        $html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) . '</div>';
1623e3a9f44cSAtari911                    }
1624e3a9f44cSAtari911
1625e3a9f44cSAtari911                    $html .= '</div>'; // item
162619378907SAtari911                }
162719378907SAtari911            }
162887ac9bf3SAtari911        }
162919378907SAtari911
1630e3a9f44cSAtari911        $html .= '</div>'; // eventlist-simple
163119378907SAtari911
163219378907SAtari911        return $html;
163319378907SAtari911    }
163419378907SAtari911
16350c3b6e81SAtari911    private function renderEventDialog($calId, $namespace, $theme = null) {
16369ccd446eSAtari911        // Get theme for dialog
16370c3b6e81SAtari911        if ($theme === null) {
16389ccd446eSAtari911            $theme = $this->getSidebarTheme();
16390c3b6e81SAtari911        }
16409ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
16419ccd446eSAtari911
164219378907SAtari911        $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">';
164319378907SAtari911        $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>';
164419378907SAtari911
16459ccd446eSAtari911        // Draggable dialog with theme
164619378907SAtari911        $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">';
164719378907SAtari911
164819378907SAtari911        // Header with drag handle and close button
164919378907SAtari911        $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">';
165019378907SAtari911        $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>';
165119378907SAtari911        $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>';
165219378907SAtari911        $html .= '</div>';
165319378907SAtari911
165419378907SAtari911        // Form content
165519378907SAtari911        $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">';
165619378907SAtari911
165719378907SAtari911        // Hidden ID field
165819378907SAtari911        $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">';
165919378907SAtari911
16601d05cddcSAtari911        // 1. TITLE
16611d05cddcSAtari911        $html .= '<div class="form-field">';
16621d05cddcSAtari911        $html .= '<label class="field-label">�� Title</label>';
16631d05cddcSAtari911        $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek input-compact" placeholder="Event or task title...">';
166419378907SAtari911        $html .= '</div>';
166519378907SAtari911
16661d05cddcSAtari911        // 1.5 NAMESPACE SELECTOR (Searchable with fuzzy matching)
16671d05cddcSAtari911        $html .= '<div class="form-field">';
16681d05cddcSAtari911        $html .= '<label class="field-label">�� Namespace</label>';
16691d05cddcSAtari911
16701d05cddcSAtari911        // Hidden field to store actual selected namespace
16711d05cddcSAtari911        $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">';
16721d05cddcSAtari911
16731d05cddcSAtari911        // Searchable input
16741d05cddcSAtari911        $html .= '<div class="namespace-search-wrapper">';
16751d05cddcSAtari911        $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">';
16761d05cddcSAtari911        $html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="display:none;"></div>';
16771d05cddcSAtari911        $html .= '</div>';
16781d05cddcSAtari911
16791d05cddcSAtari911        // Store namespaces as JSON for JavaScript
16801d05cddcSAtari911        $allNamespaces = $this->getAllNamespaces();
16811d05cddcSAtari911        $html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($allNamespaces) . '</script>';
16821d05cddcSAtari911
16831d05cddcSAtari911        $html .= '</div>';
16841d05cddcSAtari911
16851d05cddcSAtari911        // 2. DESCRIPTION
16861d05cddcSAtari911        $html .= '<div class="form-field">';
16871d05cddcSAtari911        $html .= '<label class="field-label">�� Description</label>';
16889ccd446eSAtari911        $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="2" class="input-sleek textarea-sleek textarea-compact" placeholder="Optional details..."></textarea>';
16891d05cddcSAtari911        $html .= '</div>';
16901d05cddcSAtari911
16911d05cddcSAtari911        // 3. START DATE - END DATE (inline)
169219378907SAtari911        $html .= '<div class="form-row-group">';
169319378907SAtari911
16941d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
16951d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Start Date</label>';
16961d05cddcSAtari911        $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date input-compact">';
169719378907SAtari911        $html .= '</div>';
169819378907SAtari911
16991d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
17001d05cddcSAtari911        $html .= '<label class="field-label-compact">�� End Date</label>';
17011d05cddcSAtari911        $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date input-compact" placeholder="Optional">';
170219378907SAtari911        $html .= '</div>';
170319378907SAtari911
17041d05cddcSAtari911        $html .= '</div>'; // End row
170519378907SAtari911
17061d05cddcSAtari911        // 4. IS REPEATING CHECKBOX
17071d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
17081d05cddcSAtari911        $html .= '<label class="checkbox-label checkbox-label-compact">';
170987ac9bf3SAtari911        $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">';
171087ac9bf3SAtari911        $html .= '<span>�� Repeating Event</span>';
171187ac9bf3SAtari911        $html .= '</label>';
171287ac9bf3SAtari911        $html .= '</div>';
171387ac9bf3SAtari911
17141d05cddcSAtari911        // Recurring options (shown when checkbox is checked)
1715*96df7d3eSAtari911        $html .= '<div id="recurring-options-' . $calId . '" class="recurring-options" style="display:none; border:1px solid var(--border-color, #333); border-radius:4px; padding:8px; margin:4px 0; background:var(--background-alt, rgba(0,0,0,0.2));">';
171687ac9bf3SAtari911
1717*96df7d3eSAtari911        // Row 1: Repeat every [N] [period]
1718*96df7d3eSAtari911        $html .= '<div class="form-row-group" style="margin-bottom:6px;">';
17191d05cddcSAtari911
1720*96df7d3eSAtari911        $html .= '<div class="form-field" style="flex:0 0 auto; min-width:0;">';
1721*96df7d3eSAtari911        $html .= '<label class="field-label-compact">Repeat every</label>';
1722*96df7d3eSAtari911        $html .= '<input type="number" id="event-recurrence-interval-' . $calId . '" name="recurrenceInterval" class="input-sleek input-compact" value="1" min="1" max="99" style="width:50px;">';
1723*96df7d3eSAtari911        $html .= '</div>';
1724*96df7d3eSAtari911
1725*96df7d3eSAtari911        $html .= '<div class="form-field" style="flex:1; min-width:0;">';
1726*96df7d3eSAtari911        $html .= '<label class="field-label-compact">&nbsp;</label>';
1727*96df7d3eSAtari911        $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek input-compact" onchange="updateRecurrenceOptions(\'' . $calId . '\')">';
1728*96df7d3eSAtari911        $html .= '<option value="daily">Day(s)</option>';
1729*96df7d3eSAtari911        $html .= '<option value="weekly">Week(s)</option>';
1730*96df7d3eSAtari911        $html .= '<option value="monthly">Month(s)</option>';
1731*96df7d3eSAtari911        $html .= '<option value="yearly">Year(s)</option>';
173287ac9bf3SAtari911        $html .= '</select>';
173387ac9bf3SAtari911        $html .= '</div>';
173487ac9bf3SAtari911
1735*96df7d3eSAtari911        $html .= '</div>'; // End row 1
1736*96df7d3eSAtari911
1737*96df7d3eSAtari911        // Row 2: Weekly options - day of week checkboxes
1738*96df7d3eSAtari911        $html .= '<div id="weekly-options-' . $calId . '" class="weekly-options" style="display:none; margin-bottom:6px;">';
1739*96df7d3eSAtari911        $html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">On these days:</label>';
1740*96df7d3eSAtari911        $html .= '<div style="display:flex; flex-wrap:wrap; gap:2px;">';
1741*96df7d3eSAtari911        $dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
1742*96df7d3eSAtari911        foreach ($dayNames as $idx => $day) {
1743*96df7d3eSAtari911            $html .= '<label style="display:inline-flex; align-items:center; padding:2px 6px; background:var(--cell-bg, #1a1a1a); border:1px solid var(--border-color, #333); border-radius:3px; cursor:pointer; font-size:10px;">';
1744*96df7d3eSAtari911            $html .= '<input type="checkbox" name="weekDays[]" value="' . $idx . '" style="margin-right:3px; width:12px; height:12px;">';
1745*96df7d3eSAtari911            $html .= '<span>' . $day . '</span>';
1746*96df7d3eSAtari911            $html .= '</label>';
1747*96df7d3eSAtari911        }
1748*96df7d3eSAtari911        $html .= '</div>';
1749*96df7d3eSAtari911        $html .= '</div>'; // End weekly options
1750*96df7d3eSAtari911
1751*96df7d3eSAtari911        // Row 3: Monthly options - day of month OR ordinal weekday
1752*96df7d3eSAtari911        $html .= '<div id="monthly-options-' . $calId . '" class="monthly-options" style="display:none; margin-bottom:6px;">';
1753*96df7d3eSAtari911        $html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">Repeat on:</label>';
1754*96df7d3eSAtari911
1755*96df7d3eSAtari911        // Radio: Day of month vs Ordinal weekday
1756*96df7d3eSAtari911        $html .= '<div style="margin-bottom:6px;">';
1757*96df7d3eSAtari911        $html .= '<label style="display:inline-flex; align-items:center; margin-right:12px; cursor:pointer; font-size:11px;">';
1758*96df7d3eSAtari911        $html .= '<input type="radio" name="monthlyType" value="dayOfMonth" checked onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">';
1759*96df7d3eSAtari911        $html .= 'Day of month';
1760*96df7d3eSAtari911        $html .= '</label>';
1761*96df7d3eSAtari911        $html .= '<label style="display:inline-flex; align-items:center; cursor:pointer; font-size:11px;">';
1762*96df7d3eSAtari911        $html .= '<input type="radio" name="monthlyType" value="ordinalWeekday" onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">';
1763*96df7d3eSAtari911        $html .= 'Weekday pattern';
1764*96df7d3eSAtari911        $html .= '</label>';
176587ac9bf3SAtari911        $html .= '</div>';
176687ac9bf3SAtari911
1767*96df7d3eSAtari911        // Day of month input (shown by default)
1768*96df7d3eSAtari911        $html .= '<div id="monthly-day-' . $calId . '" style="display:flex; align-items:center; gap:6px;">';
1769*96df7d3eSAtari911        $html .= '<span style="font-size:11px;">Day</span>';
1770*96df7d3eSAtari911        $html .= '<input type="number" id="event-month-day-' . $calId . '" name="monthDay" class="input-sleek input-compact" value="1" min="1" max="31" style="width:50px;">';
1771*96df7d3eSAtari911        $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">of each month</span>';
1772*96df7d3eSAtari911        $html .= '</div>';
1773*96df7d3eSAtari911
1774*96df7d3eSAtari911        // Ordinal weekday (hidden by default)
1775*96df7d3eSAtari911        $html .= '<div id="monthly-ordinal-' . $calId . '" style="display:none;">';
1776*96df7d3eSAtari911        $html .= '<div style="display:flex; align-items:center; gap:4px; flex-wrap:wrap;">';
1777*96df7d3eSAtari911        $html .= '<select id="event-ordinal-' . $calId . '" name="ordinalWeek" class="input-sleek input-compact" style="width:auto;">';
1778*96df7d3eSAtari911        $html .= '<option value="1">First</option>';
1779*96df7d3eSAtari911        $html .= '<option value="2">Second</option>';
1780*96df7d3eSAtari911        $html .= '<option value="3">Third</option>';
1781*96df7d3eSAtari911        $html .= '<option value="4">Fourth</option>';
1782*96df7d3eSAtari911        $html .= '<option value="5">Fifth</option>';
1783*96df7d3eSAtari911        $html .= '<option value="-1">Last</option>';
1784*96df7d3eSAtari911        $html .= '</select>';
1785*96df7d3eSAtari911        $html .= '<select id="event-ordinal-day-' . $calId . '" name="ordinalDay" class="input-sleek input-compact" style="width:auto;">';
1786*96df7d3eSAtari911        $html .= '<option value="0">Sunday</option>';
1787*96df7d3eSAtari911        $html .= '<option value="1">Monday</option>';
1788*96df7d3eSAtari911        $html .= '<option value="2">Tuesday</option>';
1789*96df7d3eSAtari911        $html .= '<option value="3">Wednesday</option>';
1790*96df7d3eSAtari911        $html .= '<option value="4">Thursday</option>';
1791*96df7d3eSAtari911        $html .= '<option value="5">Friday</option>';
1792*96df7d3eSAtari911        $html .= '<option value="6">Saturday</option>';
1793*96df7d3eSAtari911        $html .= '</select>';
1794*96df7d3eSAtari911        $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">of each month</span>';
1795*96df7d3eSAtari911        $html .= '</div>';
1796*96df7d3eSAtari911        $html .= '</div>';
1797*96df7d3eSAtari911
1798*96df7d3eSAtari911        $html .= '</div>'; // End monthly options
1799*96df7d3eSAtari911
1800*96df7d3eSAtari911        // Row 4: End date
1801*96df7d3eSAtari911        $html .= '<div class="form-row-group">';
1802*96df7d3eSAtari911        $html .= '<div class="form-field">';
1803*96df7d3eSAtari911        $html .= '<label class="field-label-compact">Repeat Until (optional)</label>';
1804*96df7d3eSAtari911        $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date input-compact" placeholder="Optional">';
1805*96df7d3eSAtari911        $html .= '<div style="font-size:9px; color:var(--text-dim, #666); margin-top:2px;">Leave empty for 1 year of events</div>';
1806*96df7d3eSAtari911        $html .= '</div>';
1807*96df7d3eSAtari911        $html .= '</div>'; // End row 4
1808*96df7d3eSAtari911
18091d05cddcSAtari911        $html .= '</div>'; // End recurring options
181087ac9bf3SAtari911
18111d05cddcSAtari911        // 5. TIME (Start & End) - COLOR (inline)
18121d05cddcSAtari911        $html .= '<div class="form-row-group">';
18131d05cddcSAtari911
18141d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
18151d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Start Time</label>';
18161d05cddcSAtari911        $html .= '<select id="event-time-' . $calId . '" name="time" class="input-sleek input-compact" onchange="updateEndTimeOptions(\'' . $calId . '\')">';
18171d05cddcSAtari911        $html .= '<option value="">All day</option>';
1818e3a9f44cSAtari911
1819e3a9f44cSAtari911        // Generate time options in 15-minute intervals
1820e3a9f44cSAtari911        for ($hour = 0; $hour < 24; $hour++) {
1821e3a9f44cSAtari911            for ($minute = 0; $minute < 60; $minute += 15) {
1822e3a9f44cSAtari911                $timeValue = sprintf('%02d:%02d', $hour, $minute);
1823e3a9f44cSAtari911                $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
1824e3a9f44cSAtari911                $ampm = $hour < 12 ? 'AM' : 'PM';
1825e3a9f44cSAtari911                $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
1826e3a9f44cSAtari911                $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
1827e3a9f44cSAtari911            }
1828e3a9f44cSAtari911        }
1829e3a9f44cSAtari911
1830e3a9f44cSAtari911        $html .= '</select>';
183119378907SAtari911        $html .= '</div>';
183219378907SAtari911
18331d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
18341d05cddcSAtari911        $html .= '<label class="field-label-compact">�� End Time</label>';
18351d05cddcSAtari911        $html .= '<select id="event-end-time-' . $calId . '" name="endTime" class="input-sleek input-compact">';
18361d05cddcSAtari911        $html .= '<option value="">Same as start</option>';
18371d05cddcSAtari911
18381d05cddcSAtari911        // Generate time options in 15-minute intervals
18391d05cddcSAtari911        for ($hour = 0; $hour < 24; $hour++) {
18401d05cddcSAtari911            for ($minute = 0; $minute < 60; $minute += 15) {
18411d05cddcSAtari911                $timeValue = sprintf('%02d:%02d', $hour, $minute);
18421d05cddcSAtari911                $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
18431d05cddcSAtari911                $ampm = $hour < 12 ? 'AM' : 'PM';
18441d05cddcSAtari911                $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
18451d05cddcSAtari911                $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
18461d05cddcSAtari911            }
18471d05cddcSAtari911        }
18481d05cddcSAtari911
18491d05cddcSAtari911        $html .= '</select>';
185019378907SAtari911        $html .= '</div>';
185119378907SAtari911
18521d05cddcSAtari911        $html .= '</div>'; // End row
18531d05cddcSAtari911
18541d05cddcSAtari911        // Color field (new row)
18551d05cddcSAtari911        $html .= '<div class="form-row-group">';
18561d05cddcSAtari911
18571d05cddcSAtari911        $html .= '<div class="form-field form-field-full">';
18581d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Color</label>';
18591d05cddcSAtari911        $html .= '<div class="color-picker-wrapper">';
18601d05cddcSAtari911        $html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact color-select" onchange="updateCustomColorPicker(\'' . $calId . '\')">';
18611d05cddcSAtari911        $html .= '<option value="#3498db" style="background:#3498db;color:white">�� Blue</option>';
18621d05cddcSAtari911        $html .= '<option value="#2ecc71" style="background:#2ecc71;color:white">�� Green</option>';
18631d05cddcSAtari911        $html .= '<option value="#e74c3c" style="background:#e74c3c;color:white">�� Red</option>';
18641d05cddcSAtari911        $html .= '<option value="#f39c12" style="background:#f39c12;color:white">�� Orange</option>';
18651d05cddcSAtari911        $html .= '<option value="#9b59b6" style="background:#9b59b6;color:white">�� Purple</option>';
18661d05cddcSAtari911        $html .= '<option value="#e91e63" style="background:#e91e63;color:white">�� Pink</option>';
18671d05cddcSAtari911        $html .= '<option value="#1abc9c" style="background:#1abc9c;color:white">�� Teal</option>';
18681d05cddcSAtari911        $html .= '<option value="custom">�� Custom...</option>';
18691d05cddcSAtari911        $html .= '</select>';
18701d05cddcSAtari911        $html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input color-picker-compact" value="#3498db" onchange="updateColorFromPicker(\'' . $calId . '\')">';
18711d05cddcSAtari911        $html .= '</div>';
187219378907SAtari911        $html .= '</div>';
187319378907SAtari911
18741d05cddcSAtari911        $html .= '</div>'; // End row
18751d05cddcSAtari911
18761d05cddcSAtari911        // Task checkbox
18771d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
18781d05cddcSAtari911        $html .= '<label class="checkbox-label checkbox-label-compact">';
18791d05cddcSAtari911        $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">';
18801d05cddcSAtari911        $html .= '<span>�� This is a task (can be checked off)</span>';
18811d05cddcSAtari911        $html .= '</label>';
188219378907SAtari911        $html .= '</div>';
188319378907SAtari911
188419378907SAtari911        // Action buttons
188519378907SAtari911        $html .= '<div class="dialog-actions-sleek">';
188619378907SAtari911        $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>';
188719378907SAtari911        $html .= '<button type="submit" class="btn-sleek btn-save-sleek">�� Save</button>';
188819378907SAtari911        $html .= '</div>';
188919378907SAtari911
189019378907SAtari911        $html .= '</form>';
189119378907SAtari911        $html .= '</div>';
189219378907SAtari911        $html .= '</div>';
189319378907SAtari911
189419378907SAtari911        return $html;
189519378907SAtari911    }
189619378907SAtari911
18979ccd446eSAtari911    private function renderMonthPicker($calId, $year, $month, $namespace, $theme = 'matrix', $themeStyles = null) {
18989ccd446eSAtari911        // Fallback to default theme if not provided
18999ccd446eSAtari911        if ($themeStyles === null) {
19009ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
19019ccd446eSAtari911        }
19029ccd446eSAtari911
19039ccd446eSAtari911        $themeClass = 'calendar-theme-' . $theme;
19049ccd446eSAtari911
19059ccd446eSAtari911        $html = '<div class="month-picker-overlay ' . $themeClass . '" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">';
190687ac9bf3SAtari911        $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">';
190787ac9bf3SAtari911        $html .= '<h4>Jump to Month</h4>';
190887ac9bf3SAtari911
190987ac9bf3SAtari911        $html .= '<div class="month-picker-selects">';
191087ac9bf3SAtari911        $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">';
191187ac9bf3SAtari911        $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
191287ac9bf3SAtari911        for ($m = 1; $m <= 12; $m++) {
191387ac9bf3SAtari911            $selected = ($m == $month) ? ' selected' : '';
191487ac9bf3SAtari911            $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>';
191587ac9bf3SAtari911        }
191687ac9bf3SAtari911        $html .= '</select>';
191787ac9bf3SAtari911
191887ac9bf3SAtari911        $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">';
191987ac9bf3SAtari911        $currentYear = (int)date('Y');
192087ac9bf3SAtari911        for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) {
192187ac9bf3SAtari911            $selected = ($y == $year) ? ' selected' : '';
192287ac9bf3SAtari911            $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>';
192387ac9bf3SAtari911        }
192487ac9bf3SAtari911        $html .= '</select>';
192587ac9bf3SAtari911        $html .= '</div>';
192687ac9bf3SAtari911
192787ac9bf3SAtari911        $html .= '<div class="month-picker-actions">';
192887ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>';
192987ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>';
193087ac9bf3SAtari911        $html .= '</div>';
193187ac9bf3SAtari911
193287ac9bf3SAtari911        $html .= '</div>';
193387ac9bf3SAtari911        $html .= '</div>';
193487ac9bf3SAtari911
193587ac9bf3SAtari911        return $html;
193687ac9bf3SAtari911    }
193787ac9bf3SAtari911
19389ccd446eSAtari911    private function renderDescription($description, $themeStyles = null) {
193919378907SAtari911        if (empty($description)) {
194019378907SAtari911            return '';
194119378907SAtari911        }
194219378907SAtari911
19439ccd446eSAtari911        // Get theme for link colors if not provided
19449ccd446eSAtari911        if ($themeStyles === null) {
19459ccd446eSAtari911            $theme = $this->getSidebarTheme();
19469ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
19479ccd446eSAtari911        }
19489ccd446eSAtari911
19499ccd446eSAtari911        $linkColor = '';
19509ccd446eSAtari911        $linkStyle = ' class="cal-link"';
19519ccd446eSAtari911
1952e3a9f44cSAtari911        // Token-based parsing to avoid escaping issues
1953e3a9f44cSAtari911        $rendered = $description;
1954e3a9f44cSAtari911        $tokens = array();
1955e3a9f44cSAtari911        $tokenIndex = 0;
195619378907SAtari911
1957e3a9f44cSAtari911        // Convert DokuWiki image syntax {{image.jpg}} to tokens
1958e3a9f44cSAtari911        $pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/';
1959e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1960e3a9f44cSAtari911        foreach ($matches as $match) {
1961e3a9f44cSAtari911            $imagePath = trim($match[1]);
1962e3a9f44cSAtari911            $alt = isset($match[2]) ? trim($match[2]) : '';
196319378907SAtari911
1964e3a9f44cSAtari911            // Handle external URLs
196519378907SAtari911            if (preg_match('/^https?:\/\//', $imagePath)) {
1966e3a9f44cSAtari911                $imageHtml = '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
1967e3a9f44cSAtari911            } else {
196819378907SAtari911                // Handle internal DokuWiki images
196919378907SAtari911                $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath);
1970e3a9f44cSAtari911                $imageHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
1971e3a9f44cSAtari911            }
197219378907SAtari911
1973e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
1974e3a9f44cSAtari911            $tokens[$tokenIndex] = $imageHtml;
1975e3a9f44cSAtari911            $tokenIndex++;
1976e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
1977e3a9f44cSAtari911        }
1978e3a9f44cSAtari911
1979e3a9f44cSAtari911        // Convert DokuWiki link syntax [[link|text]] to tokens
1980e3a9f44cSAtari911        $pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/';
1981e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1982e3a9f44cSAtari911        foreach ($matches as $match) {
1983e3a9f44cSAtari911            $link = trim($match[1]);
1984e3a9f44cSAtari911            $text = isset($match[2]) ? trim($match[2]) : $link;
198519378907SAtari911
198619378907SAtari911            // Handle external URLs
198719378907SAtari911            if (preg_match('/^https?:\/\//', $link)) {
19889ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
1989e3a9f44cSAtari911            } else {
199087ac9bf3SAtari911                // Handle internal DokuWiki links with section anchors
199187ac9bf3SAtari911                $parts = explode('#', $link, 2);
199287ac9bf3SAtari911                $pagePart = $parts[0];
199387ac9bf3SAtari911                $sectionPart = isset($parts[1]) ? '#' . $parts[1] : '';
199487ac9bf3SAtari911
199587ac9bf3SAtari911                $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart;
19969ccd446eSAtari911                $linkHtml = '<a href="' . $wikiUrl . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
199719378907SAtari911            }
199819378907SAtari911
1999e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2000e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
2001e3a9f44cSAtari911            $tokenIndex++;
2002e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
2003e3a9f44cSAtari911        }
200419378907SAtari911
2005e3a9f44cSAtari911        // Convert markdown-style links [text](url) to tokens
2006e3a9f44cSAtari911        $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
2007e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2008e3a9f44cSAtari911        foreach ($matches as $match) {
2009e3a9f44cSAtari911            $text = trim($match[1]);
2010e3a9f44cSAtari911            $url = trim($match[2]);
201119378907SAtari911
2012e3a9f44cSAtari911            if (preg_match('/^https?:\/\//', $url)) {
20139ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
2014e3a9f44cSAtari911            } else {
20159ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
2016e3a9f44cSAtari911            }
2017e3a9f44cSAtari911
2018e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2019e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
2020e3a9f44cSAtari911            $tokenIndex++;
2021e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
2022e3a9f44cSAtari911        }
2023e3a9f44cSAtari911
2024e3a9f44cSAtari911        // Convert plain URLs to tokens
2025e3a9f44cSAtari911        $pattern = '/(https?:\/\/[^\s<]+)/';
2026e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2027e3a9f44cSAtari911        foreach ($matches as $match) {
2028e3a9f44cSAtari911            $url = $match[1];
20299ccd446eSAtari911            $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($url) . '</a>';
2030e3a9f44cSAtari911
2031e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2032e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
2033e3a9f44cSAtari911            $tokenIndex++;
2034e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
2035e3a9f44cSAtari911        }
2036e3a9f44cSAtari911
2037e3a9f44cSAtari911        // NOW escape HTML (tokens are protected)
2038e3a9f44cSAtari911        $rendered = htmlspecialchars($rendered);
2039e3a9f44cSAtari911
2040e3a9f44cSAtari911        // Convert newlines to <br>
2041e3a9f44cSAtari911        $rendered = nl2br($rendered);
2042e3a9f44cSAtari911
2043e3a9f44cSAtari911        // DokuWiki text formatting
2044e3a9f44cSAtari911        // Bold: **text** or __text__
20459ccd446eSAtari911        $boldStyle = '';
2046e3a9f44cSAtari911        $rendered = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $rendered);
2047e3a9f44cSAtari911        $rendered = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $rendered);
2048e3a9f44cSAtari911
2049e3a9f44cSAtari911        // Italic: //text//
2050e3a9f44cSAtari911        $rendered = preg_replace('/\/\/(.+?)\/\//', '<em>$1</em>', $rendered);
2051e3a9f44cSAtari911
2052e3a9f44cSAtari911        // Strikethrough: <del>text</del>
2053e3a9f44cSAtari911        $rendered = preg_replace('/&lt;del&gt;(.+?)&lt;\/del&gt;/', '<del>$1</del>', $rendered);
2054e3a9f44cSAtari911
2055e3a9f44cSAtari911        // Monospace: ''text''
2056e3a9f44cSAtari911        $rendered = preg_replace('/&#039;&#039;(.+?)&#039;&#039;/', '<code>$1</code>', $rendered);
2057e3a9f44cSAtari911
2058e3a9f44cSAtari911        // Subscript: <sub>text</sub>
2059e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sub&gt;(.+?)&lt;\/sub&gt;/', '<sub>$1</sub>', $rendered);
2060e3a9f44cSAtari911
2061e3a9f44cSAtari911        // Superscript: <sup>text</sup>
2062e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sup&gt;(.+?)&lt;\/sup&gt;/', '<sup>$1</sup>', $rendered);
2063e3a9f44cSAtari911
2064e3a9f44cSAtari911        // Restore tokens
2065e3a9f44cSAtari911        foreach ($tokens as $i => $html) {
2066e3a9f44cSAtari911            $rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered);
2067e3a9f44cSAtari911        }
206819378907SAtari911
206919378907SAtari911        return $rendered;
207019378907SAtari911    }
207119378907SAtari911
207219378907SAtari911    private function loadEvents($namespace, $year, $month) {
207319378907SAtari911        $dataDir = DOKU_INC . 'data/meta/';
207419378907SAtari911        if ($namespace) {
207519378907SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
207619378907SAtari911        }
207719378907SAtari911        $dataDir .= 'calendar/';
207819378907SAtari911
207919378907SAtari911        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
208019378907SAtari911
208119378907SAtari911        if (file_exists($eventFile)) {
208219378907SAtari911            $json = file_get_contents($eventFile);
208319378907SAtari911            return json_decode($json, true);
208419378907SAtari911        }
208519378907SAtari911
208619378907SAtari911        return array();
208719378907SAtari911    }
2088e3a9f44cSAtari911
2089e3a9f44cSAtari911    private function loadEventsMultiNamespace($namespaces, $year, $month) {
2090e3a9f44cSAtari911        // Check for wildcard pattern (namespace:*)
2091e3a9f44cSAtari911        if (preg_match('/^(.+):\*$/', $namespaces, $matches)) {
2092e3a9f44cSAtari911            $baseNamespace = $matches[1];
2093e3a9f44cSAtari911            return $this->loadEventsWildcard($baseNamespace, $year, $month);
2094e3a9f44cSAtari911        }
2095e3a9f44cSAtari911
2096e3a9f44cSAtari911        // Check for root wildcard (just *)
2097e3a9f44cSAtari911        if ($namespaces === '*') {
2098e3a9f44cSAtari911            return $this->loadEventsWildcard('', $year, $month);
2099e3a9f44cSAtari911        }
2100e3a9f44cSAtari911
2101e3a9f44cSAtari911        // Parse namespace list (semicolon separated)
2102e3a9f44cSAtari911        // e.g., "team:projects;personal;work:tasks" = three namespaces
2103e3a9f44cSAtari911        $namespaceList = array_map('trim', explode(';', $namespaces));
2104e3a9f44cSAtari911
2105e3a9f44cSAtari911        // Load events from all namespaces
2106e3a9f44cSAtari911        $allEvents = array();
2107e3a9f44cSAtari911        foreach ($namespaceList as $ns) {
2108e3a9f44cSAtari911            $ns = trim($ns);
2109e3a9f44cSAtari911            if (empty($ns)) continue;
2110e3a9f44cSAtari911
2111e3a9f44cSAtari911            $events = $this->loadEvents($ns, $year, $month);
2112e3a9f44cSAtari911
2113e3a9f44cSAtari911            // Add namespace tag to each event
2114e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
2115e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
2116e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
2117e3a9f44cSAtari911                }
2118e3a9f44cSAtari911                foreach ($dayEvents as $event) {
2119e3a9f44cSAtari911                    $event['_namespace'] = $ns;
2120e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
2121e3a9f44cSAtari911                }
2122e3a9f44cSAtari911            }
2123e3a9f44cSAtari911        }
2124e3a9f44cSAtari911
2125e3a9f44cSAtari911        return $allEvents;
2126e3a9f44cSAtari911    }
2127e3a9f44cSAtari911
2128e3a9f44cSAtari911    private function loadEventsWildcard($baseNamespace, $year, $month) {
2129e3a9f44cSAtari911        // Find all subdirectories under the base namespace
2130e3a9f44cSAtari911        $dataDir = DOKU_INC . 'data/meta/';
2131e3a9f44cSAtari911        if ($baseNamespace) {
2132e3a9f44cSAtari911            $dataDir .= str_replace(':', '/', $baseNamespace) . '/';
2133e3a9f44cSAtari911        }
2134e3a9f44cSAtari911
2135e3a9f44cSAtari911        $allEvents = array();
2136e3a9f44cSAtari911
2137e3a9f44cSAtari911        // First, load events from the base namespace itself
2138e3a9f44cSAtari911        if (empty($baseNamespace)) {
2139e3a9f44cSAtari911            // Root wildcard - load from root calendar
2140e3a9f44cSAtari911            $events = $this->loadEvents('', $year, $month);
2141e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
2142e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
2143e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
2144e3a9f44cSAtari911                }
2145e3a9f44cSAtari911                foreach ($dayEvents as $event) {
2146e3a9f44cSAtari911                    $event['_namespace'] = '';
2147e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
2148e3a9f44cSAtari911                }
2149e3a9f44cSAtari911            }
2150e3a9f44cSAtari911        } else {
2151e3a9f44cSAtari911            $events = $this->loadEvents($baseNamespace, $year, $month);
2152e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
2153e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
2154e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
2155e3a9f44cSAtari911                }
2156e3a9f44cSAtari911                foreach ($dayEvents as $event) {
2157e3a9f44cSAtari911                    $event['_namespace'] = $baseNamespace;
2158e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
2159e3a9f44cSAtari911                }
2160e3a9f44cSAtari911            }
2161e3a9f44cSAtari911        }
2162e3a9f44cSAtari911
2163e3a9f44cSAtari911        // Recursively find all subdirectories
2164e3a9f44cSAtari911        $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents);
2165e3a9f44cSAtari911
2166e3a9f44cSAtari911        return $allEvents;
2167e3a9f44cSAtari911    }
2168e3a9f44cSAtari911
2169e3a9f44cSAtari911    private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) {
2170e3a9f44cSAtari911        if (!is_dir($dir)) return;
2171e3a9f44cSAtari911
2172e3a9f44cSAtari911        $items = scandir($dir);
2173e3a9f44cSAtari911        foreach ($items as $item) {
2174e3a9f44cSAtari911            if ($item === '.' || $item === '..') continue;
2175e3a9f44cSAtari911
2176e3a9f44cSAtari911            $path = $dir . $item;
2177e3a9f44cSAtari911            if (is_dir($path) && $item !== 'calendar') {
2178e3a9f44cSAtari911                // This is a namespace directory
2179e3a9f44cSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
2180e3a9f44cSAtari911
2181e3a9f44cSAtari911                // Load events from this namespace
2182e3a9f44cSAtari911                $events = $this->loadEvents($namespace, $year, $month);
2183e3a9f44cSAtari911                foreach ($events as $dateKey => $dayEvents) {
2184e3a9f44cSAtari911                    if (!isset($allEvents[$dateKey])) {
2185e3a9f44cSAtari911                        $allEvents[$dateKey] = array();
2186e3a9f44cSAtari911                    }
2187e3a9f44cSAtari911                    foreach ($dayEvents as $event) {
2188e3a9f44cSAtari911                        $event['_namespace'] = $namespace;
2189e3a9f44cSAtari911                        $allEvents[$dateKey][] = $event;
2190e3a9f44cSAtari911                    }
2191e3a9f44cSAtari911                }
2192e3a9f44cSAtari911
2193e3a9f44cSAtari911                // Recurse into subdirectories
2194e3a9f44cSAtari911                $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents);
2195e3a9f44cSAtari911            }
2196e3a9f44cSAtari911        }
2197e3a9f44cSAtari911    }
21981d05cddcSAtari911
21991d05cddcSAtari911    private function getAllNamespaces() {
22001d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
22011d05cddcSAtari911        $namespaces = [];
22021d05cddcSAtari911
22031d05cddcSAtari911        // Scan for namespaces that have calendar data
22041d05cddcSAtari911        $this->scanForCalendarNamespaces($dataDir, '', $namespaces);
22051d05cddcSAtari911
22061d05cddcSAtari911        // Sort alphabetically
22071d05cddcSAtari911        sort($namespaces);
22081d05cddcSAtari911
22091d05cddcSAtari911        return $namespaces;
22101d05cddcSAtari911    }
22111d05cddcSAtari911
22121d05cddcSAtari911    private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) {
22131d05cddcSAtari911        if (!is_dir($dir)) return;
22141d05cddcSAtari911
22151d05cddcSAtari911        $items = scandir($dir);
22161d05cddcSAtari911        foreach ($items as $item) {
22171d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
22181d05cddcSAtari911
22191d05cddcSAtari911            $path = $dir . $item;
22201d05cddcSAtari911            if (is_dir($path)) {
22211d05cddcSAtari911                // Check if this directory has a calendar subdirectory with data
22221d05cddcSAtari911                $calendarDir = $path . '/calendar/';
22231d05cddcSAtari911                if (is_dir($calendarDir)) {
22241d05cddcSAtari911                    // Check if there are any JSON files in the calendar directory
22251d05cddcSAtari911                    $jsonFiles = glob($calendarDir . '*.json');
22261d05cddcSAtari911                    if (!empty($jsonFiles)) {
22271d05cddcSAtari911                        // This namespace has calendar data
22281d05cddcSAtari911                        $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
22291d05cddcSAtari911                        $namespaces[] = $namespace;
22301d05cddcSAtari911                    }
22311d05cddcSAtari911                }
22321d05cddcSAtari911
22331d05cddcSAtari911                // Recurse into subdirectories
22341d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
22351d05cddcSAtari911                $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces);
22361d05cddcSAtari911            }
22371d05cddcSAtari911        }
22381d05cddcSAtari911    }
22391d05cddcSAtari911
22401d05cddcSAtari911    /**
22411d05cddcSAtari911     * Render new sidebar widget - Week at a glance itinerary (200px wide)
22421d05cddcSAtari911     */
22430c3b6e81SAtari911    private function renderSidebarWidget($events, $namespace, $calId, $themeOverride = null) {
22441d05cddcSAtari911        if (empty($events)) {
22451d05cddcSAtari911            return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">No events this week</div>';
22461d05cddcSAtari911        }
22471d05cddcSAtari911
22481d05cddcSAtari911        // Get important namespaces from config
22491d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
22501d05cddcSAtari911        $importantNsList = ['important']; // default
22511d05cddcSAtari911        if (file_exists($configFile)) {
22521d05cddcSAtari911            $config = include $configFile;
22531d05cddcSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
22541d05cddcSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
22551d05cddcSAtari911            }
22561d05cddcSAtari911        }
22571d05cddcSAtari911
22581d05cddcSAtari911        // Calculate date ranges
22591d05cddcSAtari911        $todayStr = date('Y-m-d');
22601d05cddcSAtari911        $tomorrowStr = date('Y-m-d', strtotime('+1 day'));
22619ccd446eSAtari911
22629ccd446eSAtari911        // Get week start preference and calculate week range
22639ccd446eSAtari911        $weekStartDay = $this->getWeekStartDay();
22649ccd446eSAtari911
22659ccd446eSAtari911        if ($weekStartDay === 'monday') {
22669ccd446eSAtari911            // Monday start
22671d05cddcSAtari911            $weekStart = date('Y-m-d', strtotime('monday this week'));
22681d05cddcSAtari911            $weekEnd = date('Y-m-d', strtotime('sunday this week'));
22699ccd446eSAtari911        } else {
22709ccd446eSAtari911            // Sunday start (default - US/Canada standard)
22719ccd446eSAtari911            $today = date('w'); // 0 (Sun) to 6 (Sat)
22729ccd446eSAtari911            if ($today == 0) {
22739ccd446eSAtari911                // Today is Sunday
22749ccd446eSAtari911                $weekStart = date('Y-m-d');
22759ccd446eSAtari911            } else {
22769ccd446eSAtari911                // Monday-Saturday: go back to last Sunday
22779ccd446eSAtari911                $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
22789ccd446eSAtari911            }
22799ccd446eSAtari911            $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
22809ccd446eSAtari911        }
22811d05cddcSAtari911
22821d05cddcSAtari911        // Group events by category
22831d05cddcSAtari911        $todayEvents = [];
22841d05cddcSAtari911        $tomorrowEvents = [];
22851d05cddcSAtari911        $importantEvents = [];
22861d05cddcSAtari911        $weekEvents = []; // For week grid
22871d05cddcSAtari911
22881d05cddcSAtari911        // Process all events
22891d05cddcSAtari911        foreach ($events as $dateKey => $dayEvents) {
22909ccd446eSAtari911            // Detect conflicts for events on this day
22919ccd446eSAtari911            $eventsWithConflicts = $this->detectTimeConflicts($dayEvents);
22921d05cddcSAtari911
22939ccd446eSAtari911            foreach ($eventsWithConflicts as $event) {
22949ccd446eSAtari911                // Always categorize Today and Tomorrow regardless of week boundaries
22959ccd446eSAtari911                if ($dateKey === $todayStr) {
22969ccd446eSAtari911                    $todayEvents[] = array_merge($event, ['date' => $dateKey]);
22979ccd446eSAtari911                }
22989ccd446eSAtari911                if ($dateKey === $tomorrowStr) {
22999ccd446eSAtari911                    $tomorrowEvents[] = array_merge($event, ['date' => $dateKey]);
23009ccd446eSAtari911                }
23019ccd446eSAtari911
23029ccd446eSAtari911                // Process week grid events (only for current week)
23031d05cddcSAtari911                if ($dateKey >= $weekStart && $dateKey <= $weekEnd) {
23049ccd446eSAtari911                    // Initialize week grid day if not exists
23051d05cddcSAtari911                    if (!isset($weekEvents[$dateKey])) {
23061d05cddcSAtari911                        $weekEvents[$dateKey] = [];
23071d05cddcSAtari911                    }
23081d05cddcSAtari911
23091d05cddcSAtari911                    // Pre-render DokuWiki syntax to HTML for JavaScript display
23101d05cddcSAtari911                    $eventWithHtml = $event;
23111d05cddcSAtari911                    if (isset($event['title'])) {
23121d05cddcSAtari911                        $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']);
23131d05cddcSAtari911                    }
23141d05cddcSAtari911                    if (isset($event['description'])) {
23151d05cddcSAtari911                        $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']);
23161d05cddcSAtari911                    }
23171d05cddcSAtari911                    $weekEvents[$dateKey][] = $eventWithHtml;
23181d05cddcSAtari911                }
23191d05cddcSAtari911
23201d05cddcSAtari911                // Check if this is an important namespace
23211d05cddcSAtari911                $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
23221d05cddcSAtari911                $isImportant = false;
23231d05cddcSAtari911                foreach ($importantNsList as $impNs) {
23241d05cddcSAtari911                    if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
23251d05cddcSAtari911                        $isImportant = true;
23261d05cddcSAtari911                        break;
23271d05cddcSAtari911                    }
23281d05cddcSAtari911                }
23291d05cddcSAtari911
23309ccd446eSAtari911                // Important events: show from today through next 2 weeks
23319ccd446eSAtari911                if ($isImportant && $dateKey >= $todayStr) {
23321d05cddcSAtari911                    $importantEvents[] = array_merge($event, ['date' => $dateKey]);
23331d05cddcSAtari911                }
23341d05cddcSAtari911            }
23351d05cddcSAtari911        }
23369ccd446eSAtari911
23379ccd446eSAtari911        // Sort Important Events by date (earliest first)
23389ccd446eSAtari911        usort($importantEvents, function($a, $b) {
23399ccd446eSAtari911            $dateA = isset($a['date']) ? $a['date'] : '';
23409ccd446eSAtari911            $dateB = isset($b['date']) ? $b['date'] : '';
23419ccd446eSAtari911
23429ccd446eSAtari911            // Compare dates
23439ccd446eSAtari911            if ($dateA === $dateB) {
23449ccd446eSAtari911                // Same date - sort by time
23459ccd446eSAtari911                $timeA = isset($a['time']) ? $a['time'] : '';
23469ccd446eSAtari911                $timeB = isset($b['time']) ? $b['time'] : '';
23479ccd446eSAtari911
23489ccd446eSAtari911                if (empty($timeA) && !empty($timeB)) return 1;  // All-day events last
23499ccd446eSAtari911                if (!empty($timeA) && empty($timeB)) return -1;
23509ccd446eSAtari911                if (empty($timeA) && empty($timeB)) return 0;
23519ccd446eSAtari911
23529ccd446eSAtari911                // Both have times
23539ccd446eSAtari911                $aMinutes = $this->timeToMinutes($timeA);
23549ccd446eSAtari911                $bMinutes = $this->timeToMinutes($timeB);
23559ccd446eSAtari911                return $aMinutes - $bMinutes;
23561d05cddcSAtari911            }
23571d05cddcSAtari911
23589ccd446eSAtari911            return strcmp($dateA, $dateB);
23599ccd446eSAtari911        });
23609ccd446eSAtari911
23610c3b6e81SAtari911        // Get theme - prefer override from syntax parameter, fall back to admin default
23620c3b6e81SAtari911        $theme = !empty($themeOverride) ? $themeOverride : $this->getSidebarTheme();
23639ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
23649ccd446eSAtari911        $themeClass = 'sidebar-' . $theme;
23659ccd446eSAtari911
23669ccd446eSAtari911        // Start building HTML - Dynamic width with default font (overflow:visible for tooltips)
23679ccd446eSAtari911        $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;">';
23689ccd446eSAtari911
23699ccd446eSAtari911        // Inject CSS variables so the event dialog (shared component) picks up the theme
23709ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
23719ccd446eSAtari911        $html .= '<style>
23729ccd446eSAtari911        #sidebar-widget-' . $calId . ' {
23739ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
23749ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
23759ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
23769ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
23779ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
23789ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
23799ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
23809ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
23819ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
23829ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
23839ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
23849ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
23859ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
23869ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
23879ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
23887e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
23897e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
23907e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
23917e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
23927e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
23937e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
23947e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
23959ccd446eSAtari911        }
23969ccd446eSAtari911        </style>';
23979ccd446eSAtari911
23989ccd446eSAtari911        // Add sparkle effect for pink theme
23999ccd446eSAtari911        if ($theme === 'pink') {
24009ccd446eSAtari911            $html .= '<style>
24019ccd446eSAtari911            @keyframes sparkle-' . $calId . ' {
24029ccd446eSAtari911                0% {
24039ccd446eSAtari911                    opacity: 0;
24049ccd446eSAtari911                    transform: translate(0, 0) scale(0) rotate(0deg);
24059ccd446eSAtari911                }
24069ccd446eSAtari911                50% {
24079ccd446eSAtari911                    opacity: 1;
24089ccd446eSAtari911                    transform: translate(var(--tx), var(--ty)) scale(1) rotate(180deg);
24099ccd446eSAtari911                }
24109ccd446eSAtari911                100% {
24119ccd446eSAtari911                    opacity: 0;
24129ccd446eSAtari911                    transform: translate(calc(var(--tx) * 2), calc(var(--ty) * 2)) scale(0) rotate(360deg);
24139ccd446eSAtari911                }
24149ccd446eSAtari911            }
24159ccd446eSAtari911
24169ccd446eSAtari911            @keyframes pulse-glow-' . $calId . ' {
24179ccd446eSAtari911                0%, 100% { box-shadow: 0 0 10px rgba(255, 20, 147, 0.4); }
24189ccd446eSAtari911                50% { box-shadow: 0 0 25px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4); }
24199ccd446eSAtari911            }
24209ccd446eSAtari911
24219ccd446eSAtari911            @keyframes shimmer-' . $calId . ' {
24229ccd446eSAtari911                0% { background-position: -200% center; }
24239ccd446eSAtari911                100% { background-position: 200% center; }
24249ccd446eSAtari911            }
24259ccd446eSAtari911
24269ccd446eSAtari911            .sidebar-pink {
24279ccd446eSAtari911                animation: pulse-glow-' . $calId . ' 3s ease-in-out infinite;
24289ccd446eSAtari911            }
24299ccd446eSAtari911
24309ccd446eSAtari911            .sidebar-pink:hover {
24319ccd446eSAtari911                box-shadow: 0 0 30px rgba(255, 20, 147, 0.9), 0 0 50px rgba(255, 20, 147, 0.5) !important;
24329ccd446eSAtari911            }
24339ccd446eSAtari911
24349ccd446eSAtari911            .sparkle-' . $calId . ' {
24359ccd446eSAtari911                position: absolute;
24369ccd446eSAtari911                pointer-events: none;
24379ccd446eSAtari911                font-size: 20px;
24389ccd446eSAtari911                z-index: 1000;
24399ccd446eSAtari911                animation: sparkle-' . $calId . ' 1s ease-out forwards;
24409ccd446eSAtari911                filter: drop-shadow(0 0 3px rgba(255, 20, 147, 0.8));
24419ccd446eSAtari911            }
24429ccd446eSAtari911            </style>';
24439ccd446eSAtari911
24449ccd446eSAtari911            $html .= '<script>
24459ccd446eSAtari911            (function() {
24469ccd446eSAtari911                const container = document.getElementById("sidebar-widget-' . $calId . '");
24479ccd446eSAtari911                const sparkles = ["✨", "��", "��", "⭐", "��", "��", "��", "��", "��", "��"];
24489ccd446eSAtari911
24499ccd446eSAtari911                function createSparkle(x, y) {
24509ccd446eSAtari911                    const sparkle = document.createElement("div");
24519ccd446eSAtari911                    sparkle.className = "sparkle-' . $calId . '";
24529ccd446eSAtari911                    sparkle.textContent = sparkles[Math.floor(Math.random() * sparkles.length)];
24539ccd446eSAtari911                    sparkle.style.left = x + "px";
24549ccd446eSAtari911                    sparkle.style.top = y + "px";
24559ccd446eSAtari911
24569ccd446eSAtari911                    // Random direction
24579ccd446eSAtari911                    const angle = Math.random() * Math.PI * 2;
24589ccd446eSAtari911                    const distance = 30 + Math.random() * 40;
24599ccd446eSAtari911                    sparkle.style.setProperty("--tx", Math.cos(angle) * distance + "px");
24609ccd446eSAtari911                    sparkle.style.setProperty("--ty", Math.sin(angle) * distance + "px");
24619ccd446eSAtari911
24629ccd446eSAtari911                    container.appendChild(sparkle);
24639ccd446eSAtari911
24649ccd446eSAtari911                    setTimeout(() => sparkle.remove(), 1000);
24659ccd446eSAtari911                }
24669ccd446eSAtari911
24679ccd446eSAtari911                // Click sparkles
24689ccd446eSAtari911                container.addEventListener("click", function(e) {
24699ccd446eSAtari911                    const rect = container.getBoundingClientRect();
24709ccd446eSAtari911                    const x = e.clientX - rect.left;
24719ccd446eSAtari911                    const y = e.clientY - rect.top;
24729ccd446eSAtari911
24739ccd446eSAtari911                    // Create LOTS of sparkles for maximum bling!
24749ccd446eSAtari911                    for (let i = 0; i < 8; i++) {
24759ccd446eSAtari911                        setTimeout(() => {
24769ccd446eSAtari911                            const offsetX = x + (Math.random() - 0.5) * 30;
24779ccd446eSAtari911                            const offsetY = y + (Math.random() - 0.5) * 30;
24789ccd446eSAtari911                            createSparkle(offsetX, offsetY);
24799ccd446eSAtari911                        }, i * 40);
24809ccd446eSAtari911                    }
24819ccd446eSAtari911                });
24829ccd446eSAtari911
24839ccd446eSAtari911                // Random auto-sparkles for extra glamour
24849ccd446eSAtari911                setInterval(() => {
24859ccd446eSAtari911                    const x = Math.random() * container.offsetWidth;
24869ccd446eSAtari911                    const y = Math.random() * container.offsetHeight;
24879ccd446eSAtari911                    createSparkle(x, y);
24889ccd446eSAtari911                }, 3000);
24899ccd446eSAtari911            })();
24909ccd446eSAtari911            </script>';
24919ccd446eSAtari911        }
24921d05cddcSAtari911
24931d05cddcSAtari911        // Sanitize calId for use in JavaScript variable names (remove dashes)
24941d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
24951d05cddcSAtari911
24961d05cddcSAtari911        // CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it
24971d05cddcSAtari911        $html .= '<script>
24981d05cddcSAtari911(function() {
24991d05cddcSAtari911    // Shared state for system stats and tooltips
25001d05cddcSAtari911    const sharedState_' . $jsCalId . ' = {
25011d05cddcSAtari911        latestStats: {
25021d05cddcSAtari911            load: {"1min": 0, "5min": 0, "15min": 0},
25031d05cddcSAtari911            uptime: "",
25041d05cddcSAtari911            memory_details: {},
25051d05cddcSAtari911            top_processes: []
25061d05cddcSAtari911        },
25071d05cddcSAtari911        cpuHistory: [],
25081d05cddcSAtari911        CPU_HISTORY_SIZE: 2
25091d05cddcSAtari911    };
25101d05cddcSAtari911
25111d05cddcSAtari911    // Tooltip functions - MUST be defined before HTML uses them
25121d05cddcSAtari911    window["showTooltip_' . $jsCalId . '"] = function(color) {
25131d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
25141d05cddcSAtari911        if (!tooltip) {
25151d05cddcSAtari911            console.log("Tooltip element not found for color:", color);
25161d05cddcSAtari911            return;
25171d05cddcSAtari911        }
25181d05cddcSAtari911
25191d05cddcSAtari911        const latestStats = sharedState_' . $jsCalId . '.latestStats;
25201d05cddcSAtari911        let content = "";
25211d05cddcSAtari911
25221d05cddcSAtari911        if (color === "green") {
25231d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">CPU Load Average</div>";
25241d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
25251d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
25261d05cddcSAtari911            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
25271d05cddcSAtari911            if (latestStats.uptime) {
25287e8ea635SAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_bright'] . ';\\">Uptime: " + latestStats.uptime + "</div>";
25291d05cddcSAtari911            }
25307e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['text_bright'] . '", "important");
25317e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['text_bright'] . '", "important");
25327e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_bright'] . '", "important");
25331d05cddcSAtari911        } else if (color === "purple") {
25341d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">CPU Load (Short-term)</div>";
25351d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
25361d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
25371d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
25387e8ea635SAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['border'] . ';\\" class=\\"tooltip-title\\">Top Processes</div>";
25391d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
25401d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
25411d05cddcSAtari911                });
25421d05cddcSAtari911            }
25437e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['border'] . '", "important");
25447e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['border'] . '", "important");
25457e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['border'] . '", "important");
25461d05cddcSAtari911        } else if (color === "orange") {
25471d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">Memory Usage</div>";
25481d05cddcSAtari911            if (latestStats.memory_details && latestStats.memory_details.total) {
25491d05cddcSAtari911                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
25501d05cddcSAtari911                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
25511d05cddcSAtari911                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
25521d05cddcSAtari911                if (latestStats.memory_details.cached) {
25531d05cddcSAtari911                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
25541d05cddcSAtari911                }
25551d05cddcSAtari911            } else {
25561d05cddcSAtari911                content += "<div>Loading...</div>";
25571d05cddcSAtari911            }
25581d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
25597e8ea635SAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_primary'] . ';\\" class=\\"tooltip-title\\">Top Processes</div>";
25601d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
25611d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
25621d05cddcSAtari911                });
25631d05cddcSAtari911            }
25647e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['text_primary'] . '", "important");
25657e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['text_primary'] . '", "important");
25667e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_primary'] . '", "important");
25671d05cddcSAtari911        }
25681d05cddcSAtari911
25691d05cddcSAtari911        tooltip.innerHTML = content;
25707e8ea635SAtari911        tooltip.style.setProperty("display", "block");
25717e8ea635SAtari911        tooltip.style.setProperty("background", "' . $themeStyles['bg'] . '", "important");
25721d05cddcSAtari911
25731d05cddcSAtari911        const bar = tooltip.parentElement;
25741d05cddcSAtari911        const barRect = bar.getBoundingClientRect();
25751d05cddcSAtari911        const tooltipRect = tooltip.getBoundingClientRect();
25761d05cddcSAtari911
25771d05cddcSAtari911        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
25781d05cddcSAtari911        const top = barRect.top - tooltipRect.height - 8;
25791d05cddcSAtari911
25801d05cddcSAtari911        tooltip.style.left = left + "px";
25811d05cddcSAtari911        tooltip.style.top = top + "px";
25821d05cddcSAtari911    };
25831d05cddcSAtari911
25841d05cddcSAtari911    window["hideTooltip_' . $jsCalId . '"] = function(color) {
25851d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
25861d05cddcSAtari911        if (tooltip) {
25871d05cddcSAtari911            tooltip.style.display = "none";
25881d05cddcSAtari911        }
25891d05cddcSAtari911    };
25901d05cddcSAtari911
25911d05cddcSAtari911    // Update clock every second
25921d05cddcSAtari911    function updateClock() {
25931d05cddcSAtari911        const now = new Date();
25941d05cddcSAtari911        let hours = now.getHours();
25951d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
25961d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
25971d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
25981d05cddcSAtari911        hours = hours % 12 || 12;
25991d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
26001d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
26011d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
26021d05cddcSAtari911    }
26031d05cddcSAtari911    setInterval(updateClock, 1000);
26041d05cddcSAtari911
2605*96df7d3eSAtari911    // Weather - uses default location, click weather to get local
2606*96df7d3eSAtari911    var userLocationGranted = false;
2607*96df7d3eSAtari911    var userLat = 38.5816;  // Sacramento default
2608*96df7d3eSAtari911    var userLon = -121.4944;
26091d05cddcSAtari911
2610*96df7d3eSAtari911    function fetchWeatherData(lat, lon) {
2611*96df7d3eSAtari911        fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_weather=true&temperature_unit=fahrenheit")
26121d05cddcSAtari911            .then(response => response.json())
26131d05cddcSAtari911            .then(data => {
26141d05cddcSAtari911                if (data.current_weather) {
26151d05cddcSAtari911                    const temp = Math.round(data.current_weather.temperature);
26161d05cddcSAtari911                    const weatherCode = data.current_weather.weathercode;
26171d05cddcSAtari911                    const icon = getWeatherIcon(weatherCode);
26181d05cddcSAtari911                    const iconEl = document.getElementById("weather-icon-' . $calId . '");
26191d05cddcSAtari911                    const tempEl = document.getElementById("weather-temp-' . $calId . '");
26201d05cddcSAtari911                    if (iconEl) iconEl.textContent = icon;
26211d05cddcSAtari911                    if (tempEl) tempEl.innerHTML = temp + "&deg;";
26221d05cddcSAtari911                }
26231d05cddcSAtari911            })
26241d05cddcSAtari911            .catch(error => console.log("Weather fetch error:", error));
2625*96df7d3eSAtari911    }
2626*96df7d3eSAtari911
2627*96df7d3eSAtari911    function updateWeather() {
2628*96df7d3eSAtari911        fetchWeatherData(userLat, userLon);
2629*96df7d3eSAtari911    }
2630*96df7d3eSAtari911
2631*96df7d3eSAtari911    // Click weather icon to request local weather (user gesture required)
2632*96df7d3eSAtari911    function requestLocalWeather() {
2633*96df7d3eSAtari911        if (userLocationGranted) return;
2634*96df7d3eSAtari911        if ("geolocation" in navigator) {
2635*96df7d3eSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
2636*96df7d3eSAtari911                userLat = position.coords.latitude;
2637*96df7d3eSAtari911                userLon = position.coords.longitude;
2638*96df7d3eSAtari911                userLocationGranted = true;
2639*96df7d3eSAtari911                fetchWeatherData(userLat, userLon);
26401d05cddcSAtari911            }, function(error) {
2641*96df7d3eSAtari911                console.log("Geolocation denied, using default location");
26421d05cddcSAtari911            });
26431d05cddcSAtari911        }
26441d05cddcSAtari911    }
26451d05cddcSAtari911
2646*96df7d3eSAtari911    setTimeout(function() {
2647*96df7d3eSAtari911        var weatherEl = document.querySelector("#weather-icon-' . $calId . '");
2648*96df7d3eSAtari911        if (weatherEl) {
2649*96df7d3eSAtari911            weatherEl.style.cursor = "pointer";
2650*96df7d3eSAtari911            weatherEl.title = "Click for local weather";
2651*96df7d3eSAtari911            weatherEl.addEventListener("click", requestLocalWeather);
2652*96df7d3eSAtari911        }
2653*96df7d3eSAtari911    }, 100);
2654*96df7d3eSAtari911
26551d05cddcSAtari911    function getWeatherIcon(code) {
26561d05cddcSAtari911        const icons = {
26571d05cddcSAtari911            0: "☀️", 1: "��️", 2: "⛅", 3: "☁️",
26581d05cddcSAtari911            45: "��️", 48: "��️", 51: "��️", 53: "��️", 55: "��️",
26591d05cddcSAtari911            61: "��️", 63: "��️", 65: "⛈️", 71: "��️", 73: "��️",
26601d05cddcSAtari911            75: "❄️", 77: "��️", 80: "��️", 81: "��️", 82: "⛈️",
26611d05cddcSAtari911            85: "��️", 86: "❄️", 95: "⛈️", 96: "⛈️", 99: "⛈️"
26621d05cddcSAtari911        };
26631d05cddcSAtari911        return icons[code] || "��️";
26641d05cddcSAtari911    }
26651d05cddcSAtari911
26661d05cddcSAtari911    // Update weather immediately and every 10 minutes
26671d05cddcSAtari911    updateWeather();
26681d05cddcSAtari911    setInterval(updateWeather, 600000);
26691d05cddcSAtari911
26701d05cddcSAtari911    // Update system stats and tooltips data
26711d05cddcSAtari911    function updateSystemStats() {
26721d05cddcSAtari911        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
26731d05cddcSAtari911            .then(response => response.json())
26741d05cddcSAtari911            .then(data => {
26751d05cddcSAtari911                sharedState_' . $jsCalId . '.latestStats = {
26761d05cddcSAtari911                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
26771d05cddcSAtari911                    uptime: data.uptime || "",
26781d05cddcSAtari911                    memory_details: data.memory_details || {},
26791d05cddcSAtari911                    top_processes: data.top_processes || []
26801d05cddcSAtari911                };
26811d05cddcSAtari911
26821d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
26831d05cddcSAtari911                if (greenBar) {
26841d05cddcSAtari911                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
26851d05cddcSAtari911                }
26861d05cddcSAtari911
26871d05cddcSAtari911                sharedState_' . $jsCalId . '.cpuHistory.push(data.cpu);
26881d05cddcSAtari911                if (sharedState_' . $jsCalId . '.cpuHistory.length > sharedState_' . $jsCalId . '.CPU_HISTORY_SIZE) {
26891d05cddcSAtari911                    sharedState_' . $jsCalId . '.cpuHistory.shift();
26901d05cddcSAtari911                }
26911d05cddcSAtari911
26921d05cddcSAtari911                const cpuAverage = sharedState_' . $jsCalId . '.cpuHistory.reduce((sum, val) => sum + val, 0) / sharedState_' . $jsCalId . '.cpuHistory.length;
26931d05cddcSAtari911
26941d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
26951d05cddcSAtari911                if (cpuBar) {
26961d05cddcSAtari911                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
26971d05cddcSAtari911                }
26981d05cddcSAtari911
26991d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
27001d05cddcSAtari911                if (memBar) {
27011d05cddcSAtari911                    memBar.style.width = Math.min(100, data.memory) + "%";
27021d05cddcSAtari911                }
27031d05cddcSAtari911            })
27041d05cddcSAtari911            .catch(error => {
27051d05cddcSAtari911                console.log("System stats error:", error);
27061d05cddcSAtari911            });
27071d05cddcSAtari911    }
27081d05cddcSAtari911
27091d05cddcSAtari911    updateSystemStats();
27101d05cddcSAtari911    setInterval(updateSystemStats, 2000);
27111d05cddcSAtari911})();
27121d05cddcSAtari911</script>';
27131d05cddcSAtari911
27141d05cddcSAtari911        // NOW add the header HTML (after JavaScript is defined)
27151d05cddcSAtari911        $todayDate = new DateTime();
27161d05cddcSAtari911        $displayDate = $todayDate->format('D, M j, Y');
27171d05cddcSAtari911        $currentTime = $todayDate->format('g:i:s A');
27181d05cddcSAtari911
27199ccd446eSAtari911        $html .= '<div class="eventlist-today-header" style="background:' . $themeStyles['header_bg'] . '; border:2px solid ' . $themeStyles['header_border'] . '; box-shadow:' . $themeStyles['header_shadow'] . ';">';
27209ccd446eSAtari911        $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '" style="color:' . $themeStyles['text_bright'] . ';">' . $currentTime . '</span>';
27211d05cddcSAtari911        $html .= '<div class="eventlist-bottom-info">';
27229ccd446eSAtari911        $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '" style="color:' . $themeStyles['text_primary'] . ';">--°</span></span>';
27239ccd446eSAtari911        $html .= '<span class="eventlist-today-date" style="color:' . $themeStyles['text_dim'] . ';">' . $displayDate . '</span>';
27241d05cddcSAtari911        $html .= '</div>';
27251d05cddcSAtari911
27261d05cddcSAtari911        // Three CPU/Memory bars (all update live)
27271d05cddcSAtari911        $html .= '<div class="eventlist-stats-container">';
27281d05cddcSAtari911
27291d05cddcSAtari911        // 5-minute load average (green, updates every 2 seconds)
27307e8ea635SAtari911        $html .= '<div class="eventlist-cpu-bar" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $jsCalId . '(\'green\')" onmouseout="hideTooltip_' . $jsCalId . '(\'green\')">';
27317e8ea635SAtari911        $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_bright'] . ' !important;"></div>';
27321d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
27331d05cddcSAtari911        $html .= '</div>';
27341d05cddcSAtari911
27351d05cddcSAtari911        // Real-time CPU (purple, updates with 5-sec average)
27367e8ea635SAtari911        $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $jsCalId . '(\'purple\')" onmouseout="hideTooltip_' . $jsCalId . '(\'purple\')">';
27377e8ea635SAtari911        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['border'] . ' !important;"></div>';
27381d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
27391d05cddcSAtari911        $html .= '</div>';
27401d05cddcSAtari911
27411d05cddcSAtari911        // Real-time Memory (orange, updates)
27427e8ea635SAtari911        $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $jsCalId . '(\'orange\')" onmouseout="hideTooltip_' . $jsCalId . '(\'orange\')">';
27437e8ea635SAtari911        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_primary'] . ' !important;"></div>';
27441d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
27451d05cddcSAtari911        $html .= '</div>';
27461d05cddcSAtari911
27471d05cddcSAtari911        $html .= '</div>';
27481d05cddcSAtari911        $html .= '</div>';
27491d05cddcSAtari911
2750231d0edbSAtari911        // Get today's date for default event date
2751231d0edbSAtari911        $todayStr = date('Y-m-d');
2752231d0edbSAtari911
27539ccd446eSAtari911        // Thin "Add Event" bar between header and week grid - theme-aware colors
27547e8ea635SAtari911        $addBtnBg = $themeStyles['cell_today_bg'];
27557e8ea635SAtari911        $addBtnHover = $themeStyles['grid_bg'];
27567e8ea635SAtari911        $addBtnTextColor = ($theme === 'professional' || $theme === 'wiki') ?
27577e8ea635SAtari911                          $themeStyles['text_bright'] : $themeStyles['text_bright'];
27587e8ea635SAtari911        $addBtnShadow = ($theme === 'professional' || $theme === 'wiki') ?
27597e8ea635SAtari911                       '0 2px 4px rgba(0,0,0,0.2)' : '0 0 8px ' . $themeStyles['shadow'];
27607e8ea635SAtari911        $addBtnHoverShadow = ($theme === 'professional' || $theme === 'wiki') ?
27617e8ea635SAtari911                            '0 3px 6px rgba(0,0,0,0.3)' : '0 0 12px ' . $themeStyles['shadow'];
27629ccd446eSAtari911
27639ccd446eSAtari911        $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 . '\';">';
27649ccd446eSAtari911        $addBtnTextShadow = ($theme === 'pink') ? '0 0 3px ' . $addBtnTextColor : 'none';
27659ccd446eSAtari911        $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>';
27661d05cddcSAtari911        $html .= '</div>';
27671d05cddcSAtari911
27681d05cddcSAtari911        // Week grid (7 cells)
27699ccd446eSAtari911        $html .= $this->renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme);
27701d05cddcSAtari911
27717e8ea635SAtari911        // Section colors - derived from theme palette
27727e8ea635SAtari911        // Today: brightest accent, Tomorrow: primary accent, Important: dim/secondary accent
27737e8ea635SAtari911        if ($theme === 'matrix') {
27747e8ea635SAtari911            $todayColor = '#00ff00';     // Bright green
27757e8ea635SAtari911            $tomorrowColor = '#00cc07';  // Standard green
27767e8ea635SAtari911            $importantColor = '#00aa00'; // Dim green
27777e8ea635SAtari911        } else if ($theme === 'purple') {
27787e8ea635SAtari911            $todayColor = '#d4a5ff';     // Bright purple
27797e8ea635SAtari911            $tomorrowColor = '#9b59b6';  // Standard purple
27807e8ea635SAtari911            $importantColor = '#8e7ab8'; // Dim purple
27817e8ea635SAtari911        } else if ($theme === 'pink') {
27827e8ea635SAtari911            $todayColor = '#ff1493';     // Hot pink
27837e8ea635SAtari911            $tomorrowColor = '#ff69b4';  // Medium pink
27847e8ea635SAtari911            $importantColor = '#ff85c1'; // Light pink
27857e8ea635SAtari911        } else if ($theme === 'professional') {
27867e8ea635SAtari911            $todayColor = '#4a90e2';     // Blue accent
27877e8ea635SAtari911            $tomorrowColor = '#5ba3e6';  // Lighter blue
27887e8ea635SAtari911            $importantColor = '#7fb8ec'; // Lightest blue
27899ccd446eSAtari911        } else {
27907e8ea635SAtari911            // Wiki - section header backgrounds from template colors
27917e8ea635SAtari911            $todayColor = $themeStyles['text_bright'];      // __link__
27927e8ea635SAtari911            $tomorrowColor = $themeStyles['header_bg'];     // __background_alt__
27937e8ea635SAtari911            $importantColor = $themeStyles['header_border'];// __border__
27949ccd446eSAtari911        }
27959ccd446eSAtari911
2796*96df7d3eSAtari911        // Check if there are any itinerary items
2797*96df7d3eSAtari911        $hasItinerary = !empty($todayEvents) || !empty($tomorrowEvents) || !empty($importantEvents);
2798*96df7d3eSAtari911
2799*96df7d3eSAtari911        // Itinerary bar (collapsible toggle) - styled like +Add bar
2800*96df7d3eSAtari911        $itineraryBg = $themeStyles['cell_today_bg'];
2801*96df7d3eSAtari911        $itineraryHover = $themeStyles['grid_bg'];
2802*96df7d3eSAtari911        $itineraryTextColor = ($theme === 'professional' || $theme === 'wiki') ?
2803*96df7d3eSAtari911                              $themeStyles['text_bright'] : $themeStyles['text_bright'];
2804*96df7d3eSAtari911        $itineraryShadow = ($theme === 'professional' || $theme === 'wiki') ?
2805*96df7d3eSAtari911                           '0 2px 4px rgba(0,0,0,0.2)' : '0 0 8px ' . $themeStyles['shadow'];
2806*96df7d3eSAtari911        $itineraryHoverShadow = ($theme === 'professional' || $theme === 'wiki') ?
2807*96df7d3eSAtari911                                '0 3px 6px rgba(0,0,0,0.3)' : '0 0 12px ' . $themeStyles['shadow'];
2808*96df7d3eSAtari911        $itineraryTextShadow = ($theme === 'pink') ? '0 0 3px ' . $itineraryTextColor : 'none';
2809*96df7d3eSAtari911
2810*96df7d3eSAtari911        // Sanitize calId for JavaScript
2811*96df7d3eSAtari911        $jsCalId = str_replace('-', '_', $calId);
2812*96df7d3eSAtari911
2813*96df7d3eSAtari911        // Get itinerary default state from settings
2814*96df7d3eSAtari911        $itineraryDefaultCollapsed = $this->getItineraryCollapsed();
2815*96df7d3eSAtari911        $arrowDefaultStyle = $itineraryDefaultCollapsed ? 'transform:rotate(-90deg);' : '';
2816*96df7d3eSAtari911        $contentDefaultStyle = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : '';
2817*96df7d3eSAtari911
2818*96df7d3eSAtari911        $html .= '<div id="itinerary-bar-' . $calId . '" style="background:' . $itineraryBg . '; 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:' . $itineraryShadow . '; transition:all 0.2s; display:flex; align-items:center; justify-content:center; gap:4px;" onclick="toggleItinerary_' . $jsCalId . '();" onmouseover="this.style.background=\'' . $itineraryHover . '\'; this.style.boxShadow=\'' . $itineraryHoverShadow . '\';" onmouseout="this.style.background=\'' . $itineraryBg . '\'; this.style.boxShadow=\'' . $itineraryShadow . '\';">';
2819*96df7d3eSAtari911        $html .= '<span id="itinerary-arrow-' . $calId . '" style="color:' . $itineraryTextColor . '; font-size:6px; font-weight:700; font-family:system-ui, sans-serif; text-shadow:' . $itineraryTextShadow . '; position:relative; top:-1px; transition:transform 0.2s; ' . $arrowDefaultStyle . '">▼</span>';
2820*96df7d3eSAtari911        $html .= '<span style="color:' . $itineraryTextColor . '; font-size:8px; font-weight:700; letter-spacing:0.4px; font-family:system-ui, sans-serif; text-shadow:' . $itineraryTextShadow . '; position:relative; top:-1px;">ITINERARY</span>';
2821*96df7d3eSAtari911        $html .= '</div>';
2822*96df7d3eSAtari911
2823*96df7d3eSAtari911        // Itinerary content container (collapsible)
2824*96df7d3eSAtari911        $html .= '<div id="itinerary-content-' . $calId . '" style="transition:max-height 0.3s ease-out, opacity 0.2s ease-out; overflow:hidden; ' . $contentDefaultStyle . '">';
2825*96df7d3eSAtari911
28269ccd446eSAtari911        // Today section
28271d05cddcSAtari911        if (!empty($todayEvents)) {
2828*96df7d3eSAtari911            $html .= $this->renderSidebarSection('Today', $todayEvents, $todayColor, $calId, $themeStyles, $theme, $importantNsList);
28291d05cddcSAtari911        }
28301d05cddcSAtari911
28319ccd446eSAtari911        // Tomorrow section
28321d05cddcSAtari911        if (!empty($tomorrowEvents)) {
2833*96df7d3eSAtari911            $html .= $this->renderSidebarSection('Tomorrow', $tomorrowEvents, $tomorrowColor, $calId, $themeStyles, $theme, $importantNsList);
28341d05cddcSAtari911        }
28351d05cddcSAtari911
28369ccd446eSAtari911        // Important events section
28371d05cddcSAtari911        if (!empty($importantEvents)) {
2838*96df7d3eSAtari911            $html .= $this->renderSidebarSection('Important Events', $importantEvents, $importantColor, $calId, $themeStyles, $theme, $importantNsList);
28391d05cddcSAtari911        }
28401d05cddcSAtari911
2841*96df7d3eSAtari911        // Empty state if no itinerary items
2842*96df7d3eSAtari911        if (!$hasItinerary) {
2843*96df7d3eSAtari911            $html .= '<div style="padding:8px; text-align:center; color:' . $themeStyles['text_dim'] . '; font-size:10px; font-family:system-ui, sans-serif;">No upcoming events</div>';
2844*96df7d3eSAtari911        }
2845*96df7d3eSAtari911
2846*96df7d3eSAtari911        $html .= '</div>'; // Close itinerary-content
2847*96df7d3eSAtari911
2848*96df7d3eSAtari911        // Get itinerary default state from settings
2849*96df7d3eSAtari911        $itineraryDefaultCollapsed = $this->getItineraryCollapsed();
2850*96df7d3eSAtari911        $itineraryExpandedDefault = $itineraryDefaultCollapsed ? 'false' : 'true';
2851*96df7d3eSAtari911        $itineraryArrowDefault = $itineraryDefaultCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
2852*96df7d3eSAtari911        $itineraryContentDefault = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : 'max-height:none;';
2853*96df7d3eSAtari911
2854*96df7d3eSAtari911        // JavaScript for toggling itinerary
2855*96df7d3eSAtari911        $html .= '<script>
2856*96df7d3eSAtari911        (function() {
2857*96df7d3eSAtari911            let itineraryExpanded_' . $jsCalId . ' = ' . $itineraryExpandedDefault . ';
2858*96df7d3eSAtari911
2859*96df7d3eSAtari911            window.toggleItinerary_' . $jsCalId . ' = function() {
2860*96df7d3eSAtari911                const content = document.getElementById("itinerary-content-' . $calId . '");
2861*96df7d3eSAtari911                const arrow = document.getElementById("itinerary-arrow-' . $calId . '");
2862*96df7d3eSAtari911
2863*96df7d3eSAtari911                if (itineraryExpanded_' . $jsCalId . ') {
2864*96df7d3eSAtari911                    // Collapse
2865*96df7d3eSAtari911                    content.style.maxHeight = "0px";
2866*96df7d3eSAtari911                    content.style.opacity = "0";
2867*96df7d3eSAtari911                    arrow.style.transform = "rotate(-90deg)";
2868*96df7d3eSAtari911                    itineraryExpanded_' . $jsCalId . ' = false;
2869*96df7d3eSAtari911                } else {
2870*96df7d3eSAtari911                    // Expand
2871*96df7d3eSAtari911                    content.style.maxHeight = content.scrollHeight + "px";
2872*96df7d3eSAtari911                    content.style.opacity = "1";
2873*96df7d3eSAtari911                    arrow.style.transform = "rotate(0deg)";
2874*96df7d3eSAtari911                    itineraryExpanded_' . $jsCalId . ' = true;
2875*96df7d3eSAtari911
2876*96df7d3eSAtari911                    // After transition, set to auto for dynamic content
2877*96df7d3eSAtari911                    setTimeout(function() {
2878*96df7d3eSAtari911                        if (itineraryExpanded_' . $jsCalId . ') {
2879*96df7d3eSAtari911                            content.style.maxHeight = "none";
2880*96df7d3eSAtari911                        }
2881*96df7d3eSAtari911                    }, 300);
2882*96df7d3eSAtari911                }
2883*96df7d3eSAtari911            };
2884*96df7d3eSAtari911
2885*96df7d3eSAtari911            // Initialize based on default state
2886*96df7d3eSAtari911            const content = document.getElementById("itinerary-content-' . $calId . '");
2887*96df7d3eSAtari911            const arrow = document.getElementById("itinerary-arrow-' . $calId . '");
2888*96df7d3eSAtari911            if (content && arrow) {
2889*96df7d3eSAtari911                if (' . $itineraryExpandedDefault . ') {
2890*96df7d3eSAtari911                    content.style.maxHeight = "none";
2891*96df7d3eSAtari911                    arrow.style.transform = "rotate(0deg)";
2892*96df7d3eSAtari911                } else {
2893*96df7d3eSAtari911                    content.style.maxHeight = "0px";
2894*96df7d3eSAtari911                    content.style.opacity = "0";
2895*96df7d3eSAtari911                    arrow.style.transform = "rotate(-90deg)";
2896*96df7d3eSAtari911                }
2897*96df7d3eSAtari911            }
2898*96df7d3eSAtari911        })();
2899*96df7d3eSAtari911        </script>';
2900*96df7d3eSAtari911
29011d05cddcSAtari911        $html .= '</div>';
29021d05cddcSAtari911
2903231d0edbSAtari911        // Add event dialog for sidebar widget
29040c3b6e81SAtari911        $html .= $this->renderEventDialog($calId, $namespace, $theme);
2905231d0edbSAtari911
29069ccd446eSAtari911        // Add JavaScript for positioning data-tooltip elements
29079ccd446eSAtari911        $html .= '<script>
29089ccd446eSAtari911        // Position data-tooltip elements to prevent cutoff (up and to the LEFT)
29099ccd446eSAtari911        document.addEventListener("DOMContentLoaded", function() {
29109ccd446eSAtari911            const tooltipElements = document.querySelectorAll("[data-tooltip]");
29119ccd446eSAtari911            const isPinkTheme = document.querySelector(".sidebar-pink") !== null;
29129ccd446eSAtari911
29139ccd446eSAtari911            tooltipElements.forEach(function(element) {
29149ccd446eSAtari911                element.addEventListener("mouseenter", function() {
29159ccd446eSAtari911                    const rect = element.getBoundingClientRect();
29169ccd446eSAtari911                    const style = window.getComputedStyle(element, ":before");
29179ccd446eSAtari911
29189ccd446eSAtari911                    // Position above the element, aligned to LEFT (not right)
29199ccd446eSAtari911                    element.style.setProperty("--tooltip-left", (rect.left - 150) + "px");
29209ccd446eSAtari911                    element.style.setProperty("--tooltip-top", (rect.top - 30) + "px");
29219ccd446eSAtari911
29229ccd446eSAtari911                    // Pink theme: position heart to the right of tooltip
29239ccd446eSAtari911                    if (isPinkTheme) {
29249ccd446eSAtari911                        element.style.setProperty("--heart-left", (rect.left - 150 + 210) + "px");
29259ccd446eSAtari911                        element.style.setProperty("--heart-top", (rect.top - 30) + "px");
29269ccd446eSAtari911                    }
29279ccd446eSAtari911                });
29289ccd446eSAtari911            });
29299ccd446eSAtari911        });
29309ccd446eSAtari911
29319ccd446eSAtari911        // Apply custom properties to position tooltips
29329ccd446eSAtari911        const style = document.createElement("style");
29339ccd446eSAtari911        style.textContent = `
29349ccd446eSAtari911            [data-tooltip]:hover:before {
29359ccd446eSAtari911                left: var(--tooltip-left, 0) !important;
29369ccd446eSAtari911                top: var(--tooltip-top, 0) !important;
29379ccd446eSAtari911            }
29389ccd446eSAtari911            .sidebar-pink [data-tooltip]:hover:after {
29399ccd446eSAtari911                left: var(--heart-left, 0) !important;
29409ccd446eSAtari911                top: var(--heart-top, 0) !important;
29419ccd446eSAtari911            }
29429ccd446eSAtari911        `;
29439ccd446eSAtari911        document.head.appendChild(style);
29449ccd446eSAtari911        </script>';
29459ccd446eSAtari911
29461d05cddcSAtari911        return $html;
29471d05cddcSAtari911    }
29481d05cddcSAtari911
29491d05cddcSAtari911    /**
29509ccd446eSAtari911     * Render compact week grid (7 cells with event bars) - Theme-aware
29511d05cddcSAtari911     */
29529ccd446eSAtari911    private function renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme) {
29531d05cddcSAtari911        // Generate unique ID for this calendar instance - sanitize for JavaScript
29541d05cddcSAtari911        $calId = 'cal_' . substr(md5($weekStart . microtime()), 0, 8);
29551d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);  // Sanitize for JS variable names
29561d05cddcSAtari911
29579ccd446eSAtari911        $html = '<div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:' . $themeStyles['grid_bg'] . '; border-bottom:2px solid ' . $themeStyles['grid_border'] . ';">';
29581d05cddcSAtari911
29599ccd446eSAtari911        // Day names depend on week start setting
29609ccd446eSAtari911        $weekStartDay = $this->getWeekStartDay();
29619ccd446eSAtari911        if ($weekStartDay === 'monday') {
29629ccd446eSAtari911            $dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];  // Monday to Sunday
29639ccd446eSAtari911        } else {
29649ccd446eSAtari911            $dayNames = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];  // Sunday to Saturday
29659ccd446eSAtari911        }
29661d05cddcSAtari911        $today = date('Y-m-d');
29671d05cddcSAtari911
29681d05cddcSAtari911        for ($i = 0; $i < 7; $i++) {
29691d05cddcSAtari911            $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days'));
29701d05cddcSAtari911            $dayNum = date('j', strtotime($date));
29711d05cddcSAtari911            $isToday = $date === $today;
29721d05cddcSAtari911
29731d05cddcSAtari911            $events = isset($weekEvents[$date]) ? $weekEvents[$date] : [];
29741d05cddcSAtari911            $eventCount = count($events);
29751d05cddcSAtari911
29769ccd446eSAtari911            $bgColor = $isToday ? $themeStyles['cell_today_bg'] : $themeStyles['cell_bg'];
29779ccd446eSAtari911            $textColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
29781d05cddcSAtari911            $fontWeight = $isToday ? '700' : '500';
29799ccd446eSAtari911
29809ccd446eSAtari911            // Theme-aware text shadow
29819ccd446eSAtari911            if ($theme === 'pink') {
29829ccd446eSAtari911                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
29837e8ea635SAtari911                $textShadow = $isToday ? 'text-shadow:0 0 3px ' . $glowColor . ';' : 'text-shadow:0 0 2px ' . $glowColor . ';';
29847e8ea635SAtari911            } else if ($theme === 'matrix') {
29857e8ea635SAtari911                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
29867e8ea635SAtari911                $textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glowColor . ';';
29877e8ea635SAtari911            } else if ($theme === 'purple') {
29887e8ea635SAtari911                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
29897e8ea635SAtari911                $textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glowColor . ';';
29909ccd446eSAtari911            } else {
29917e8ea635SAtari911                $textShadow = '';  // No glow for professional/wiki
29929ccd446eSAtari911            }
29939ccd446eSAtari911
29949ccd446eSAtari911            // Border color based on theme
29959ccd446eSAtari911            $borderColor = $themeStyles['grid_border'];
29961d05cddcSAtari911
29971d05cddcSAtari911            $hasEvents = $eventCount > 0;
29981d05cddcSAtari911            $clickableStyle = $hasEvents ? 'cursor:pointer;' : '';
29991d05cddcSAtari911            $clickHandler = $hasEvents ? ' onclick="showDayEvents_' . $jsCalId . '(\'' . $date . '\')"' : '';
30001d05cddcSAtari911
30019ccd446eSAtari911            $html .= '<div style="background:' . $bgColor . '; padding:4px 2px; text-align:center; min-height:45px; position:relative; border:1px solid ' . $borderColor . ' !important; ' . $clickableStyle . '" ' . $clickHandler . '>';
30021d05cddcSAtari911
30039ccd446eSAtari911            // Day letter - theme color
30049ccd446eSAtari911            $dayLetterColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary'];
30059ccd446eSAtari911            $html .= '<div style="font-size:9px; color:' . $dayLetterColor . '; font-weight:500; font-family:system-ui, sans-serif;">' . $dayNames[$i] . '</div>';
30061d05cddcSAtari911
30071d05cddcSAtari911            // Day number
30081d05cddcSAtari911            $html .= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:2px 0; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNum . '</div>';
30091d05cddcSAtari911
30109ccd446eSAtari911            // Event bars (max 4 visible) with theme-aware glow
30111d05cddcSAtari911            if ($eventCount > 0) {
30129ccd446eSAtari911                $showCount = min($eventCount, 4);
30131d05cddcSAtari911                for ($j = 0; $j < $showCount; $j++) {
30141d05cddcSAtari911                    $event = $events[$j];
30159ccd446eSAtari911                    $color = isset($event['color']) ? $event['color'] : $themeStyles['text_primary'];
30169ccd446eSAtari911                    $barShadow = $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . htmlspecialchars($color);
30179ccd446eSAtari911                    $html .= '<div style="height:2px; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:' . $barShadow . ';"></div>';
30181d05cddcSAtari911                }
30191d05cddcSAtari911
30209ccd446eSAtari911                // Show "+N more" if more than 4 - theme color
30219ccd446eSAtari911                if ($eventCount > 4) {
30229ccd446eSAtari911                    $moreTextColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary'];
30239ccd446eSAtari911                    $html .= '<div style="font-size:7px; color:' . $moreTextColor . '; margin-top:1px; font-family:system-ui, sans-serif;">+' . ($eventCount - 4) . '</div>';
30241d05cddcSAtari911                }
30251d05cddcSAtari911            }
30261d05cddcSAtari911
30271d05cddcSAtari911            $html .= '</div>';
30281d05cddcSAtari911        }
30291d05cddcSAtari911
30301d05cddcSAtari911        $html .= '</div>';
30311d05cddcSAtari911
30329ccd446eSAtari911        // Add container for selected day events display (with unique ID) - theme-aware
30337e8ea635SAtari911        $panelBorderColor = $themeStyles['border'];
30347e8ea635SAtari911        $panelHeaderBg = $themeStyles['border'];
30357e8ea635SAtari911        $panelShadow = ($theme === 'professional' || $theme === 'wiki') ?
30367e8ea635SAtari911                      '0 1px 3px rgba(0, 0, 0, 0.1)' :
30377e8ea635SAtari911                      '0 0 5px ' . $themeStyles['shadow'];
30387e8ea635SAtari911        $panelContentBg = ($theme === 'professional') ? 'rgba(255, 255, 255, 0.95)' :
30399ccd446eSAtari911                         ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.5)');
30409ccd446eSAtari911        $panelHeaderShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $panelHeaderBg;
30419ccd446eSAtari911
30427e8ea635SAtari911        // Header text color - dark bg text for dark themes, white for light theme accent headers
30437e8ea635SAtari911        $panelHeaderColor = ($theme === 'matrix' || $theme === 'purple' || $theme === 'pink') ? $themeStyles['bg'] :
30447e8ea635SAtari911                            (($theme === 'wiki') ? $themeStyles['text_primary'] : '#fff');
30459ccd446eSAtari911
30467e8ea635SAtari911        $html .= '<div id="selected-day-events-' . $calId . '" style="display:none; margin:8px 4px; border-left:3px solid ' . $panelBorderColor . ($theme === 'wiki' ? '' : ' !important') . '; box-shadow:' . $panelShadow . ';">';
30477e8ea635SAtari911        if ($theme === 'wiki') {
30489ccd446eSAtari911            $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;">';
30491d05cddcSAtari911            $html .= '<span id="selected-day-title-' . $calId . '"></span>';
30509ccd446eSAtari911            $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>';
30517e8ea635SAtari911        } else {
30527e8ea635SAtari911            $html .= '<div style="background:' . $panelHeaderBg . ' !important; color:' . $panelHeaderColor . ' !important; -webkit-text-fill-color:' . $panelHeaderColor . ' !important; 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;">';
30537e8ea635SAtari911            $html .= '<span id="selected-day-title-' . $calId . '"></span>';
30547e8ea635SAtari911            $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 . ' !important; -webkit-text-fill-color:' . $panelHeaderColor . ' !important;">✕</span>';
30557e8ea635SAtari911        }
30561d05cddcSAtari911        $html .= '</div>';
30579ccd446eSAtari911        $html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:' . $panelContentBg . ';"></div>';
30581d05cddcSAtari911        $html .= '</div>';
30591d05cddcSAtari911
30601d05cddcSAtari911        // Add JavaScript for day selection with event data
30611d05cddcSAtari911        $html .= '<script>';
30621d05cddcSAtari911        // Sanitize calId for JavaScript variable names
30631d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
30641d05cddcSAtari911        $html .= 'window.weekEventsData_' . $jsCalId . ' = ' . json_encode($weekEvents) . ';';
30659ccd446eSAtari911
30669ccd446eSAtari911        // Pass theme colors to JavaScript
30679ccd446eSAtari911        $jsThemeColors = json_encode([
30689ccd446eSAtari911            'text_primary' => $themeStyles['text_primary'],
30699ccd446eSAtari911            'text_bright' => $themeStyles['text_bright'],
30709ccd446eSAtari911            'text_dim' => $themeStyles['text_dim'],
30717e8ea635SAtari911            'text_shadow' => ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $themeStyles['text_primary'] :
30727e8ea635SAtari911                             ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $themeStyles['text_primary'] : ''),
30739ccd446eSAtari911            'event_bg' => $theme === 'professional' ? 'rgba(255, 255, 255, 0.5)' :
30749ccd446eSAtari911                         ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.3)'),
30759ccd446eSAtari911            'border_color' => $theme === 'professional' ? 'rgba(0, 0, 0, 0.1)' :
30769ccd446eSAtari911                             ($theme === 'purple' ? 'rgba(155, 89, 182, 0.2)' :
30779ccd446eSAtari911                             ($theme === 'pink' ? 'rgba(255, 20, 147, 0.3)' :
30789ccd446eSAtari911                             ($theme === 'wiki' ? $themeStyles['grid_border'] : 'rgba(0, 204, 7, 0.2)'))),
30799ccd446eSAtari911            'bar_shadow' => $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' :
30809ccd446eSAtari911                           ($theme === 'wiki' ? '0 1px 2px rgba(0,0,0,0.15)' : '0 0 3px')
30819ccd446eSAtari911        ]);
30829ccd446eSAtari911        $html .= 'window.themeColors_' . $jsCalId . ' = ' . $jsThemeColors . ';';
30831d05cddcSAtari911        $html .= '
30841d05cddcSAtari911        window.showDayEvents_' . $jsCalId . ' = function(dateKey) {
30851d05cddcSAtari911            const eventsData = window.weekEventsData_' . $jsCalId . ';
30861d05cddcSAtari911            const container = document.getElementById("selected-day-events-' . $calId . '");
30871d05cddcSAtari911            const title = document.getElementById("selected-day-title-' . $calId . '");
30881d05cddcSAtari911            const content = document.getElementById("selected-day-content-' . $calId . '");
30891d05cddcSAtari911
30901d05cddcSAtari911            if (!eventsData[dateKey] || eventsData[dateKey].length === 0) return;
30911d05cddcSAtari911
30921d05cddcSAtari911            // Format date for display
30931d05cddcSAtari911            const dateObj = new Date(dateKey + "T00:00:00");
30941d05cddcSAtari911            const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" });
30951d05cddcSAtari911            const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" });
30961d05cddcSAtari911            title.textContent = dayName + ", " + monthDay;
30971d05cddcSAtari911
30981d05cddcSAtari911            // Clear content
30991d05cddcSAtari911            content.innerHTML = "";
31001d05cddcSAtari911
3101231d0edbSAtari911            // Sort events by time (all-day events first, then timed events chronologically)
31021d05cddcSAtari911            const sortedEvents = [...eventsData[dateKey]].sort((a, b) => {
3103231d0edbSAtari911                // All-day events (no time) go to the beginning
31041d05cddcSAtari911                if (!a.time && !b.time) return 0;
3105231d0edbSAtari911                if (!a.time) return -1;  // a is all-day, comes first
3106231d0edbSAtari911                if (!b.time) return 1;   // b is all-day, comes first
31071d05cddcSAtari911
31081d05cddcSAtari911                // Compare times (format: "HH:MM")
31091d05cddcSAtari911                const timeA = a.time.split(":").map(Number);
31101d05cddcSAtari911                const timeB = b.time.split(":").map(Number);
31111d05cddcSAtari911                const minutesA = timeA[0] * 60 + timeA[1];
31121d05cddcSAtari911                const minutesB = timeB[0] * 60 + timeB[1];
31131d05cddcSAtari911
31141d05cddcSAtari911                return minutesA - minutesB;
31151d05cddcSAtari911            });
31161d05cddcSAtari911
31179ccd446eSAtari911            // Build events HTML with single color bar (event color only) - theme-aware
31189ccd446eSAtari911            const themeColors = window.themeColors_' . $jsCalId . ';
31191d05cddcSAtari911            sortedEvents.forEach(event => {
31209ccd446eSAtari911                const eventColor = event.color || themeColors.text_primary;
31211d05cddcSAtari911
31221d05cddcSAtari911                const eventDiv = document.createElement("div");
31239ccd446eSAtari911                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;";
31241d05cddcSAtari911
31251d05cddcSAtari911                let eventHTML = "";
31261d05cddcSAtari911
31279ccd446eSAtari911                // Event assigned color bar (single bar on left) - theme-aware shadow
31289ccd446eSAtari911                const barShadow = themeColors.bar_shadow + (themeColors.bar_shadow.includes("rgba") ? "" : " " + eventColor);
31299ccd446eSAtari911                eventHTML += "<div style=\\"width:3px; align-self:stretch; background:" + eventColor + "; border-radius:1px; flex-shrink:0; box-shadow:" + barShadow + ";\\"></div>";
31301d05cddcSAtari911
3131231d0edbSAtari911                // Content wrapper
3132231d0edbSAtari911                eventHTML += "<div style=\\"flex:1; min-width:0; display:flex; justify-content:space-between; align-items:start; gap:4px;\\">";
31331d05cddcSAtari911
3134231d0edbSAtari911                // Left side: event details
31351d05cddcSAtari911                eventHTML += "<div style=\\"flex:1; min-width:0;\\">";
31369ccd446eSAtari911                eventHTML += "<div style=\\"font-weight:600; color:" + themeColors.text_primary + "; word-wrap:break-word; font-family:system-ui, sans-serif; " + themeColors.text_shadow + ";\\">";
31371d05cddcSAtari911
31381d05cddcSAtari911                // Time
31391d05cddcSAtari911                if (event.time) {
31401d05cddcSAtari911                    const timeParts = event.time.split(":");
31411d05cddcSAtari911                    let hours = parseInt(timeParts[0]);
31421d05cddcSAtari911                    const minutes = timeParts[1];
31431d05cddcSAtari911                    const ampm = hours >= 12 ? "PM" : "AM";
31441d05cddcSAtari911                    hours = hours % 12 || 12;
31459ccd446eSAtari911                    eventHTML += "<span style=\\"color:" + themeColors.text_bright + "; font-weight:500; font-size:9px;\\">" + hours + ":" + minutes + " " + ampm + "</span> ";
31461d05cddcSAtari911                }
31471d05cddcSAtari911
31481d05cddcSAtari911                // Title - use HTML version if available
31491d05cddcSAtari911                const titleHTML = event.title_html || event.title || "Untitled";
31501d05cddcSAtari911                eventHTML += titleHTML;
31511d05cddcSAtari911                eventHTML += "</div>";
31521d05cddcSAtari911
31539ccd446eSAtari911                // Description if present - use HTML version - theme-aware color
31541d05cddcSAtari911                if (event.description_html || event.description) {
31551d05cddcSAtari911                    const descHTML = event.description_html || event.description;
31569ccd446eSAtari911                    eventHTML += "<div style=\\"font-size:9px; color:" + themeColors.text_dim + "; margin-top:2px;\\">" + descHTML + "</div>";
31571d05cddcSAtari911                }
31581d05cddcSAtari911
3159231d0edbSAtari911                eventHTML += "</div>"; // Close event details
3160231d0edbSAtari911
31619ccd446eSAtari911                // Right side: conflict badge with tooltip
3162231d0edbSAtari911                if (event.conflict) {
31639ccd446eSAtari911                    let conflictList = [];
31649ccd446eSAtari911                    if (event.conflictingWith && event.conflictingWith.length > 0) {
31659ccd446eSAtari911                        event.conflictingWith.forEach(conf => {
31669ccd446eSAtari911                            const confTime = conf.time + (conf.end_time ? " - " + conf.end_time : "");
31679ccd446eSAtari911                            conflictList.push(conf.title + " (" + confTime + ")");
31689ccd446eSAtari911                        });
31699ccd446eSAtari911                    }
31709ccd446eSAtari911                    const conflictData = btoa(unescape(encodeURIComponent(JSON.stringify(conflictList))));
31719ccd446eSAtari911                    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>";
3172231d0edbSAtari911                }
3173231d0edbSAtari911
3174231d0edbSAtari911                eventHTML += "</div>"; // Close content wrapper
31751d05cddcSAtari911
31761d05cddcSAtari911                eventDiv.innerHTML = eventHTML;
31771d05cddcSAtari911                content.appendChild(eventDiv);
31781d05cddcSAtari911            });
31791d05cddcSAtari911
31801d05cddcSAtari911            container.style.display = "block";
31811d05cddcSAtari911        };
31821d05cddcSAtari911        ';
31831d05cddcSAtari911        $html .= '</script>';
31841d05cddcSAtari911
31851d05cddcSAtari911        return $html;
31861d05cddcSAtari911    }
31871d05cddcSAtari911
31881d05cddcSAtari911    /**
31891d05cddcSAtari911     * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders
31901d05cddcSAtari911     */
3191*96df7d3eSAtari911    private function renderSidebarSection($title, $events, $accentColor, $calId, $themeStyles, $theme, $importantNsList = ['important']) {
31921d05cddcSAtari911        // Keep the original accent colors for borders
31931d05cddcSAtari911        $borderColor = $accentColor;
31941d05cddcSAtari911
31951d05cddcSAtari911        // Show date for Important Events section
31961d05cddcSAtari911        $showDate = ($title === 'Important Events');
31971d05cddcSAtari911
31989ccd446eSAtari911        // Sort events differently based on section
31999ccd446eSAtari911        if ($title === 'Important Events') {
32009ccd446eSAtari911            // Important Events: sort by date first, then by time
32019ccd446eSAtari911            usort($events, function($a, $b) {
32029ccd446eSAtari911                $aDate = isset($a['date']) ? $a['date'] : '';
32039ccd446eSAtari911                $bDate = isset($b['date']) ? $b['date'] : '';
32041d05cddcSAtari911
32059ccd446eSAtari911                // Different dates - sort by date
32069ccd446eSAtari911                if ($aDate !== $bDate) {
32079ccd446eSAtari911                    return strcmp($aDate, $bDate);
32089ccd446eSAtari911                }
32099ccd446eSAtari911
32109ccd446eSAtari911                // Same date - sort by time
32119ccd446eSAtari911                $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : '';
32129ccd446eSAtari911                $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : '';
32139ccd446eSAtari911
32149ccd446eSAtari911                // All-day events last within same date
32159ccd446eSAtari911                if (empty($aTime) && !empty($bTime)) return 1;
32169ccd446eSAtari911                if (!empty($aTime) && empty($bTime)) return -1;
32179ccd446eSAtari911                if (empty($aTime) && empty($bTime)) return 0;
32189ccd446eSAtari911
32199ccd446eSAtari911                // Both have times
32209ccd446eSAtari911                $aMinutes = $this->timeToMinutes($aTime);
32219ccd446eSAtari911                $bMinutes = $this->timeToMinutes($bTime);
32229ccd446eSAtari911                return $aMinutes - $bMinutes;
32239ccd446eSAtari911            });
32249ccd446eSAtari911        } else {
32259ccd446eSAtari911            // Today/Tomorrow: sort by time only (all same date)
32269ccd446eSAtari911            usort($events, function($a, $b) {
32279ccd446eSAtari911                $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : '';
32289ccd446eSAtari911                $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : '';
32299ccd446eSAtari911
32309ccd446eSAtari911                // All-day events (no time) come first
32319ccd446eSAtari911                if (empty($aTime) && !empty($bTime)) return -1;
32329ccd446eSAtari911                if (!empty($aTime) && empty($bTime)) return 1;
32339ccd446eSAtari911                if (empty($aTime) && empty($bTime)) return 0;
32349ccd446eSAtari911
32359ccd446eSAtari911                // Both have times - convert to minutes for proper chronological sort
32369ccd446eSAtari911                $aMinutes = $this->timeToMinutes($aTime);
32379ccd446eSAtari911                $bMinutes = $this->timeToMinutes($bTime);
32389ccd446eSAtari911
32399ccd446eSAtari911                return $aMinutes - $bMinutes;
32409ccd446eSAtari911            });
32419ccd446eSAtari911        }
32429ccd446eSAtari911
32439ccd446eSAtari911        // Theme-aware section shadow
32447e8ea635SAtari911        $sectionShadow = ($theme === 'professional' || $theme === 'wiki') ?
32457e8ea635SAtari911                        '0 1px 3px rgba(0, 0, 0, 0.1)' :
32467e8ea635SAtari911                        '0 0 5px ' . $themeStyles['shadow'];
32479ccd446eSAtari911
32487e8ea635SAtari911        if ($theme === 'wiki') {
32497e8ea635SAtari911            // Wiki theme: use a background div for the left bar instead of border-left
32507e8ea635SAtari911            // Dark Reader maps border colors differently from background colors, causing mismatch
32517e8ea635SAtari911            $html = '<div style="display:flex; margin:8px 4px; box-shadow:' . $sectionShadow . '; background:' . $themeStyles['bg'] . ';">';
32527e8ea635SAtari911            $html .= '<div style="width:3px; flex-shrink:0; background:' . $borderColor . ';"></div>';
32537e8ea635SAtari911            $html .= '<div style="flex:1; min-width:0;">';
32547e8ea635SAtari911        } else {
32557e8ea635SAtari911            $html = '<div style="border-left:3px solid ' . $borderColor . ' !important; margin:8px 4px; box-shadow:' . $sectionShadow . ';">';
32567e8ea635SAtari911        }
32579ccd446eSAtari911
32587e8ea635SAtari911        // Section header with accent color background - theme-aware
32599ccd446eSAtari911        $headerShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $accentColor;
32607e8ea635SAtari911        $headerTextColor = ($theme === 'matrix' || $theme === 'purple' || $theme === 'pink') ? $themeStyles['bg'] :
32617e8ea635SAtari911                           (($theme === 'wiki') ? $themeStyles['text_primary'] : '#fff');
32627e8ea635SAtari911        if ($theme === 'wiki') {
32637e8ea635SAtari911            // Wiki theme: no !important — let Dark Reader adjust these
32649ccd446eSAtari911            $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 . ';">';
32657e8ea635SAtari911        } else {
32667e8ea635SAtari911            // Dark themes + professional: lock colors against Dark Reader
32677e8ea635SAtari911            $html .= '<div style="background:' . $accentColor . ' !important; color:' . $headerTextColor . ' !important; -webkit-text-fill-color:' . $headerTextColor . ' !important; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:' . $headerShadow . ';">';
32687e8ea635SAtari911        }
32691d05cddcSAtari911        $html .= htmlspecialchars($title);
32701d05cddcSAtari911        $html .= '</div>';
32711d05cddcSAtari911
32729ccd446eSAtari911        // Events - no background (transparent)
32739ccd446eSAtari911        $html .= '<div style="padding:4px 0;">';
32741d05cddcSAtari911
32751d05cddcSAtari911        foreach ($events as $event) {
3276*96df7d3eSAtari911            $html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor, $themeStyles, $theme, $importantNsList);
32771d05cddcSAtari911        }
32781d05cddcSAtari911
32791d05cddcSAtari911        $html .= '</div>';
32801d05cddcSAtari911        $html .= '</div>';
32817e8ea635SAtari911        if ($theme === 'wiki') {
32827e8ea635SAtari911            $html .= '</div>'; // Close flex wrapper
32837e8ea635SAtari911        }
32841d05cddcSAtari911
32851d05cddcSAtari911        return $html;
32861d05cddcSAtari911    }
32871d05cddcSAtari911
32881d05cddcSAtari911    /**
32899ccd446eSAtari911     * Render individual event in sidebar - Theme-aware
32901d05cddcSAtari911     */
3291*96df7d3eSAtari911    private function renderSidebarEvent($event, $calId, $showDate = false, $sectionColor = '#00cc07', $themeStyles = null, $theme = 'matrix', $importantNsList = ['important']) {
32921d05cddcSAtari911        $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
32931d05cddcSAtari911        $time = isset($event['time']) ? $event['time'] : '';
32941d05cddcSAtari911        $endTime = isset($event['endTime']) ? $event['endTime'] : '';
32959ccd446eSAtari911        $eventColor = isset($event['color']) ? htmlspecialchars($event['color']) : ($themeStyles ? $themeStyles['text_primary'] : '#00cc07');
32961d05cddcSAtari911        $date = isset($event['date']) ? $event['date'] : '';
32971d05cddcSAtari911        $isTask = isset($event['isTask']) && $event['isTask'];
32981d05cddcSAtari911        $completed = isset($event['completed']) && $event['completed'];
32991d05cddcSAtari911
3300*96df7d3eSAtari911        // Check if this is an important namespace event
3301*96df7d3eSAtari911        $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
3302*96df7d3eSAtari911        $isImportantNs = false;
3303*96df7d3eSAtari911        foreach ($importantNsList as $impNs) {
3304*96df7d3eSAtari911            if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
3305*96df7d3eSAtari911                $isImportantNs = true;
3306*96df7d3eSAtari911                break;
3307*96df7d3eSAtari911            }
3308*96df7d3eSAtari911        }
3309*96df7d3eSAtari911
33109ccd446eSAtari911        // Theme-aware colors
33119ccd446eSAtari911        $titleColor = $themeStyles ? $themeStyles['text_primary'] : '#00cc07';
33129ccd446eSAtari911        $timeColor = $themeStyles ? $themeStyles['text_bright'] : '#00dd00';
33137e8ea635SAtari911        $textShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $titleColor . ';' :
33147e8ea635SAtari911                      ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $titleColor . ';' : '');
33151d05cddcSAtari911
33169ccd446eSAtari911        // Check for conflicts (using 'conflict' field set by detectTimeConflicts)
33179ccd446eSAtari911        $hasConflict = isset($event['conflict']) && $event['conflict'];
33189ccd446eSAtari911        $conflictingWith = isset($event['conflictingWith']) ? $event['conflictingWith'] : [];
33199ccd446eSAtari911
33209ccd446eSAtari911        // Build conflict list for tooltip
33219ccd446eSAtari911        $conflictList = [];
33229ccd446eSAtari911        if ($hasConflict && !empty($conflictingWith)) {
33239ccd446eSAtari911            foreach ($conflictingWith as $conf) {
33249ccd446eSAtari911                $confTime = $this->formatTimeDisplay($conf['time'], isset($conf['end_time']) ? $conf['end_time'] : '');
33259ccd446eSAtari911                $conflictList[] = $conf['title'] . ' (' . $confTime . ')';
33269ccd446eSAtari911            }
33279ccd446eSAtari911        }
33289ccd446eSAtari911
3329*96df7d3eSAtari911        // No background on individual events (transparent) - unless important namespace
33309ccd446eSAtari911        // Use theme grid_border with slight opacity for subtle divider
33319ccd446eSAtari911        $borderColor = $themeStyles['grid_border'];
33329ccd446eSAtari911
3333*96df7d3eSAtari911        // Important namespace highlighting - subtle themed background
3334*96df7d3eSAtari911        $importantBg = '';
3335*96df7d3eSAtari911        $importantBorder = '';
3336*96df7d3eSAtari911        if ($isImportantNs) {
3337*96df7d3eSAtari911            // Theme-specific important highlighting
3338*96df7d3eSAtari911            switch ($theme) {
3339*96df7d3eSAtari911                case 'matrix':
3340*96df7d3eSAtari911                    $importantBg = 'background:rgba(0,204,7,0.08);';
3341*96df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);';
3342*96df7d3eSAtari911                    break;
3343*96df7d3eSAtari911                case 'purple':
3344*96df7d3eSAtari911                    $importantBg = 'background:rgba(156,39,176,0.08);';
3345*96df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(156,39,176,0.4);';
3346*96df7d3eSAtari911                    break;
3347*96df7d3eSAtari911                case 'pink':
3348*96df7d3eSAtari911                    $importantBg = 'background:rgba(255,105,180,0.1);';
3349*96df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(255,105,180,0.5);';
3350*96df7d3eSAtari911                    break;
3351*96df7d3eSAtari911                case 'professional':
3352*96df7d3eSAtari911                    $importantBg = 'background:rgba(33,150,243,0.08);';
3353*96df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(33,150,243,0.4);';
3354*96df7d3eSAtari911                    break;
3355*96df7d3eSAtari911                case 'wiki':
3356*96df7d3eSAtari911                    $importantBg = 'background:rgba(0,102,204,0.06);';
3357*96df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(0,102,204,0.3);';
3358*96df7d3eSAtari911                    break;
3359*96df7d3eSAtari911                default:
3360*96df7d3eSAtari911                    $importantBg = 'background:rgba(0,204,7,0.08);';
3361*96df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);';
3362*96df7d3eSAtari911            }
3363*96df7d3eSAtari911        }
3364*96df7d3eSAtari911
3365*96df7d3eSAtari911        $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; ' . $importantBg . $importantBorder . '">';
33661d05cddcSAtari911
3367231d0edbSAtari911        // Event's assigned color bar (single bar on the left)
33689ccd446eSAtari911        $barShadow = ($theme === 'professional') ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . $eventColor;
33699ccd446eSAtari911        $html .= '<div style="width:3px; align-self:stretch; background:' . $eventColor . '; border-radius:1px; flex-shrink:0; box-shadow:' . $barShadow . ';"></div>';
33701d05cddcSAtari911
33711d05cddcSAtari911        // Content
33721d05cddcSAtari911        $html .= '<div style="flex:1; min-width:0;">';
33731d05cddcSAtari911
33741d05cddcSAtari911        // Time + title
33759ccd446eSAtari911        $html .= '<div style="font-weight:600; color:' . $titleColor . '; word-wrap:break-word; font-family:system-ui, sans-serif; ' . $textShadow . '">';
33761d05cddcSAtari911
33771d05cddcSAtari911        if ($time) {
33781d05cddcSAtari911            $displayTime = $this->formatTimeDisplay($time, $endTime);
33799ccd446eSAtari911            $html .= '<span style="color:' . $timeColor . '; font-weight:500; font-size:9px;">' . htmlspecialchars($displayTime) . '</span> ';
33801d05cddcSAtari911        }
33811d05cddcSAtari911
33821d05cddcSAtari911        // Task checkbox
33831d05cddcSAtari911        if ($isTask) {
33841d05cddcSAtari911            $checkIcon = $completed ? '☑' : '☐';
33859ccd446eSAtari911            $checkColor = $themeStyles ? $themeStyles['text_bright'] : '#00ff00';
33869ccd446eSAtari911            $html .= '<span style="font-size:11px; color:' . $checkColor . ';">' . $checkIcon . '</span> ';
33871d05cddcSAtari911        }
33881d05cddcSAtari911
3389*96df7d3eSAtari911        // Important indicator icon for important namespace events
3390*96df7d3eSAtari911        if ($isImportantNs) {
3391*96df7d3eSAtari911            $html .= '<span style="font-size:9px;" title="Important">⭐</span> ';
3392*96df7d3eSAtari911        }
3393*96df7d3eSAtari911
33949ccd446eSAtari911        $html .= $title; // Already HTML-escaped on line 2625
33951d05cddcSAtari911
33969ccd446eSAtari911        // Conflict badge using same system as main calendar
33979ccd446eSAtari911        if ($hasConflict && !empty($conflictList)) {
33989ccd446eSAtari911            $conflictJson = base64_encode(json_encode($conflictList));
33999ccd446eSAtari911            $html .= ' <span class="event-conflict-badge" style="font-size:10px;" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . count($conflictList) . '</span>';
34001d05cddcSAtari911        }
34011d05cddcSAtari911
34021d05cddcSAtari911        $html .= '</div>';
34031d05cddcSAtari911
34041d05cddcSAtari911        // Date display BELOW event name for Important events
34051d05cddcSAtari911        if ($showDate && $date) {
34061d05cddcSAtari911            $dateObj = new DateTime($date);
34071d05cddcSAtari911            $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10"
34089ccd446eSAtari911            $dateColor = $themeStyles ? $themeStyles['text_dim'] : '#00aa00';
34097e8ea635SAtari911            $dateShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $dateColor . ';' :
34107e8ea635SAtari911                          ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $dateColor . ';' : '');
34119ccd446eSAtari911            $html .= '<div style="font-size:8px; color:' . $dateColor . '; font-weight:500; margin-top:2px; ' . $dateShadow . '">' . htmlspecialchars($displayDate) . '</div>';
34121d05cddcSAtari911        }
34131d05cddcSAtari911
34141d05cddcSAtari911        $html .= '</div>';
34151d05cddcSAtari911        $html .= '</div>';
34161d05cddcSAtari911
34171d05cddcSAtari911        return $html;
34181d05cddcSAtari911    }
34191d05cddcSAtari911
34201d05cddcSAtari911    /**
34211d05cddcSAtari911     * Format time display (12-hour format with optional end time)
34221d05cddcSAtari911     */
34231d05cddcSAtari911    private function formatTimeDisplay($startTime, $endTime = '') {
34241d05cddcSAtari911        // Convert start time
34251d05cddcSAtari911        list($hour, $minute) = explode(':', $startTime);
34261d05cddcSAtari911        $hour = (int)$hour;
34271d05cddcSAtari911        $ampm = $hour >= 12 ? 'PM' : 'AM';
34281d05cddcSAtari911        $displayHour = $hour % 12;
34291d05cddcSAtari911        if ($displayHour === 0) $displayHour = 12;
34301d05cddcSAtari911
34311d05cddcSAtari911        $display = $displayHour . ':' . $minute . ' ' . $ampm;
34321d05cddcSAtari911
34331d05cddcSAtari911        // Add end time if provided
34341d05cddcSAtari911        if ($endTime && $endTime !== '') {
34351d05cddcSAtari911            list($endHour, $endMinute) = explode(':', $endTime);
34361d05cddcSAtari911            $endHour = (int)$endHour;
34371d05cddcSAtari911            $endAmpm = $endHour >= 12 ? 'PM' : 'AM';
34381d05cddcSAtari911            $endDisplayHour = $endHour % 12;
34391d05cddcSAtari911            if ($endDisplayHour === 0) $endDisplayHour = 12;
34401d05cddcSAtari911
34411d05cddcSAtari911            $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm;
34421d05cddcSAtari911        }
34431d05cddcSAtari911
34441d05cddcSAtari911        return $display;
34451d05cddcSAtari911    }
34461d05cddcSAtari911
34471d05cddcSAtari911    /**
34489ccd446eSAtari911     * Detect time conflicts among events on the same day
34499ccd446eSAtari911     * Returns events array with 'conflict' flag and 'conflictingWith' array
34509ccd446eSAtari911     */
34519ccd446eSAtari911    private function detectTimeConflicts($dayEvents) {
34529ccd446eSAtari911        if (empty($dayEvents)) {
34539ccd446eSAtari911            return $dayEvents;
34549ccd446eSAtari911        }
34559ccd446eSAtari911
34569ccd446eSAtari911        // If only 1 event, no conflicts possible but still add the flag
34579ccd446eSAtari911        if (count($dayEvents) === 1) {
34589ccd446eSAtari911            return [array_merge($dayEvents[0], ['conflict' => false, 'conflictingWith' => []])];
34599ccd446eSAtari911        }
34609ccd446eSAtari911
34619ccd446eSAtari911        $eventsWithFlags = [];
34629ccd446eSAtari911
34639ccd446eSAtari911        foreach ($dayEvents as $i => $event) {
34649ccd446eSAtari911            $hasConflict = false;
34659ccd446eSAtari911            $conflictingWith = [];
34669ccd446eSAtari911
34679ccd446eSAtari911            // Skip all-day events (no time)
34689ccd446eSAtari911            if (empty($event['time'])) {
34699ccd446eSAtari911                $eventsWithFlags[] = array_merge($event, ['conflict' => false, 'conflictingWith' => []]);
34709ccd446eSAtari911                continue;
34719ccd446eSAtari911            }
34729ccd446eSAtari911
34739ccd446eSAtari911            // Get this event's time range
34749ccd446eSAtari911            $startTime = $event['time'];
34759ccd446eSAtari911            // Check both 'end_time' (snake_case) and 'endTime' (camelCase) for compatibility
34769ccd446eSAtari911            $endTime = '';
34779ccd446eSAtari911            if (isset($event['end_time']) && $event['end_time'] !== '') {
34789ccd446eSAtari911                $endTime = $event['end_time'];
34799ccd446eSAtari911            } elseif (isset($event['endTime']) && $event['endTime'] !== '') {
34809ccd446eSAtari911                $endTime = $event['endTime'];
34819ccd446eSAtari911            } else {
34829ccd446eSAtari911                // If no end time, use start time (zero duration) - matches main calendar logic
34839ccd446eSAtari911                $endTime = $startTime;
34849ccd446eSAtari911            }
34859ccd446eSAtari911
34869ccd446eSAtari911            // Check against all other events
34879ccd446eSAtari911            foreach ($dayEvents as $j => $otherEvent) {
34889ccd446eSAtari911                if ($i === $j) continue; // Skip self
34899ccd446eSAtari911                if (empty($otherEvent['time'])) continue; // Skip all-day events
34909ccd446eSAtari911
34919ccd446eSAtari911                $otherStart = $otherEvent['time'];
34929ccd446eSAtari911                // Check both field name formats
34939ccd446eSAtari911                $otherEnd = '';
34949ccd446eSAtari911                if (isset($otherEvent['end_time']) && $otherEvent['end_time'] !== '') {
34959ccd446eSAtari911                    $otherEnd = $otherEvent['end_time'];
34969ccd446eSAtari911                } elseif (isset($otherEvent['endTime']) && $otherEvent['endTime'] !== '') {
34979ccd446eSAtari911                    $otherEnd = $otherEvent['endTime'];
34989ccd446eSAtari911                } else {
34999ccd446eSAtari911                    $otherEnd = $otherStart;
35009ccd446eSAtari911                }
35019ccd446eSAtari911
35029ccd446eSAtari911                // Check for overlap: convert to minutes and compare
35039ccd446eSAtari911                $start1Min = $this->timeToMinutes($startTime);
35049ccd446eSAtari911                $end1Min = $this->timeToMinutes($endTime);
35059ccd446eSAtari911                $start2Min = $this->timeToMinutes($otherStart);
35069ccd446eSAtari911                $end2Min = $this->timeToMinutes($otherEnd);
35079ccd446eSAtari911
35089ccd446eSAtari911                // Overlap if: start1 < end2 AND start2 < end1
35099ccd446eSAtari911                // Note: Using < (not <=) so events that just touch at boundaries don't conflict
35109ccd446eSAtari911                // e.g., 1:00-2:00 and 2:00-3:00 are NOT in conflict
35119ccd446eSAtari911                if ($start1Min < $end2Min && $start2Min < $end1Min) {
35129ccd446eSAtari911                    $hasConflict = true;
35139ccd446eSAtari911                    $conflictingWith[] = [
35149ccd446eSAtari911                        'title' => isset($otherEvent['title']) ? $otherEvent['title'] : 'Untitled',
35159ccd446eSAtari911                        'time' => $otherStart,
35169ccd446eSAtari911                        'end_time' => $otherEnd
35179ccd446eSAtari911                    ];
35189ccd446eSAtari911                }
35199ccd446eSAtari911            }
35209ccd446eSAtari911
35219ccd446eSAtari911            $eventsWithFlags[] = array_merge($event, [
35229ccd446eSAtari911                'conflict' => $hasConflict,
35239ccd446eSAtari911                'conflictingWith' => $conflictingWith
35249ccd446eSAtari911            ]);
35259ccd446eSAtari911        }
35269ccd446eSAtari911
35279ccd446eSAtari911        return $eventsWithFlags;
35289ccd446eSAtari911    }
35299ccd446eSAtari911
35309ccd446eSAtari911    /**
35319ccd446eSAtari911     * Add hours to a time string
35329ccd446eSAtari911     */
35339ccd446eSAtari911    private function addHoursToTime($time, $hours) {
35349ccd446eSAtari911        $totalMinutes = $this->timeToMinutes($time) + ($hours * 60);
35359ccd446eSAtari911        $h = floor($totalMinutes / 60) % 24;
35369ccd446eSAtari911        $m = $totalMinutes % 60;
35379ccd446eSAtari911        return sprintf('%02d:%02d', $h, $m);
35389ccd446eSAtari911    }
35399ccd446eSAtari911
35409ccd446eSAtari911    /**
35411d05cddcSAtari911     * Render DokuWiki syntax to HTML
35421d05cddcSAtari911     * Converts **bold**, //italic//, [[links]], etc. to HTML
35431d05cddcSAtari911     */
35441d05cddcSAtari911    private function renderDokuWikiToHtml($text) {
35451d05cddcSAtari911        if (empty($text)) return '';
35461d05cddcSAtari911
35471d05cddcSAtari911        // Use DokuWiki's parser to render the text
35481d05cddcSAtari911        $instructions = p_get_instructions($text);
35491d05cddcSAtari911
35501d05cddcSAtari911        // Render instructions to XHTML
35511d05cddcSAtari911        $xhtml = p_render('xhtml', $instructions, $info);
35521d05cddcSAtari911
35531d05cddcSAtari911        // Remove surrounding <p> tags if present (we're rendering inline)
35541d05cddcSAtari911        $xhtml = preg_replace('/^<p>(.*)<\/p>$/s', '$1', trim($xhtml));
35551d05cddcSAtari911
35561d05cddcSAtari911        return $xhtml;
35571d05cddcSAtari911    }
35581d05cddcSAtari911
35591d05cddcSAtari911    // Keep old scanForNamespaces for backward compatibility (not used anymore)
35601d05cddcSAtari911    private function scanForNamespaces($dir, $baseNamespace, &$namespaces) {
35611d05cddcSAtari911        if (!is_dir($dir)) return;
35621d05cddcSAtari911
35631d05cddcSAtari911        $items = scandir($dir);
35641d05cddcSAtari911        foreach ($items as $item) {
35651d05cddcSAtari911            if ($item === '.' || $item === '..' || $item === 'calendar') continue;
35661d05cddcSAtari911
35671d05cddcSAtari911            $path = $dir . $item;
35681d05cddcSAtari911            if (is_dir($path)) {
35691d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
35701d05cddcSAtari911                $namespaces[] = $namespace;
35711d05cddcSAtari911                $this->scanForNamespaces($path . '/', $namespace, $namespaces);
35721d05cddcSAtari911            }
35731d05cddcSAtari911        }
35741d05cddcSAtari911    }
35759ccd446eSAtari911
35769ccd446eSAtari911    /**
35779ccd446eSAtari911     * Get current sidebar theme
35789ccd446eSAtari911     */
35799ccd446eSAtari911    private function getSidebarTheme() {
35809ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_theme.txt';
35819ccd446eSAtari911        if (file_exists($configFile)) {
35829ccd446eSAtari911            $theme = trim(file_get_contents($configFile));
35839ccd446eSAtari911            if (in_array($theme, ['matrix', 'purple', 'professional', 'pink', 'wiki'])) {
35849ccd446eSAtari911                return $theme;
35859ccd446eSAtari911            }
35869ccd446eSAtari911        }
35879ccd446eSAtari911        return 'matrix'; // Default
35889ccd446eSAtari911    }
35899ccd446eSAtari911
35909ccd446eSAtari911    /**
35919ccd446eSAtari911     * Get colors from DokuWiki template's style.ini file
35929ccd446eSAtari911     */
35939ccd446eSAtari911    private function getWikiTemplateColors() {
35949ccd446eSAtari911        global $conf;
35959ccd446eSAtari911
35969ccd446eSAtari911        // Get current template name
35979ccd446eSAtari911        $template = $conf['template'];
35989ccd446eSAtari911
35999ccd446eSAtari911        // Try multiple possible locations for style.ini
36009ccd446eSAtari911        $possiblePaths = [
36019ccd446eSAtari911            DOKU_INC . 'conf/tpl/' . $template . '/style.ini',
36029ccd446eSAtari911            DOKU_INC . 'lib/tpl/' . $template . '/style.ini',
36039ccd446eSAtari911        ];
36049ccd446eSAtari911
36059ccd446eSAtari911        $styleIni = null;
36069ccd446eSAtari911        foreach ($possiblePaths as $path) {
36079ccd446eSAtari911            if (file_exists($path)) {
36089ccd446eSAtari911                $styleIni = parse_ini_file($path, true);
36099ccd446eSAtari911                break;
36109ccd446eSAtari911            }
36119ccd446eSAtari911        }
36129ccd446eSAtari911
36139ccd446eSAtari911        if (!$styleIni) {
36149ccd446eSAtari911            return null; // Fall back to CSS variables
36159ccd446eSAtari911        }
36169ccd446eSAtari911
36179ccd446eSAtari911        // Extract color replacements
36189ccd446eSAtari911        $replacements = isset($styleIni['replacements']) ? $styleIni['replacements'] : [];
36199ccd446eSAtari911
36209ccd446eSAtari911        // Map style.ini colors to our theme structure
36219ccd446eSAtari911        $bgSite = isset($replacements['__background_site__']) ? $replacements['__background_site__'] : '#f5f5f5';
36229ccd446eSAtari911        $background = isset($replacements['__background__']) ? $replacements['__background__'] : '#fff';
36239ccd446eSAtari911        $bgAlt = isset($replacements['__background_alt__']) ? $replacements['__background_alt__'] : '#e8e8e8';
36249ccd446eSAtari911        $bgNeu = isset($replacements['__background_neu__']) ? $replacements['__background_neu__'] : '#eee';
36259ccd446eSAtari911        $text = isset($replacements['__text__']) ? $replacements['__text__'] : '#333';
36269ccd446eSAtari911        $textAlt = isset($replacements['__text_alt__']) ? $replacements['__text_alt__'] : '#999';
36279ccd446eSAtari911        $textNeu = isset($replacements['__text_neu__']) ? $replacements['__text_neu__'] : '#666';
36289ccd446eSAtari911        $border = isset($replacements['__border__']) ? $replacements['__border__'] : '#ccc';
36299ccd446eSAtari911        $link = isset($replacements['__link__']) ? $replacements['__link__'] : '#2b73b7';
36309ccd446eSAtari911        $existing = isset($replacements['__existing__']) ? $replacements['__existing__'] : $link;
36319ccd446eSAtari911
36329ccd446eSAtari911        // Build theme colors from template colors
36339ccd446eSAtari911        // ============================================
36349ccd446eSAtari911        // DokuWiki style.ini → Calendar CSS Variable Mapping
36359ccd446eSAtari911        // ============================================
36369ccd446eSAtari911        //   style.ini key         → CSS variable          → Used for
36379ccd446eSAtari911        //   __background_site__   → --background-site     → Container, panel backgrounds
36389ccd446eSAtari911        //   __background__        → --cell-bg             → Cell/input backgrounds (typically white)
36399ccd446eSAtari911        //   __background_alt__    → --background-alt      → Hover states, header backgrounds
36409ccd446eSAtari911        //                         → --background-header
36419ccd446eSAtari911        //   __background_neu__    → --cell-today-bg       → Today cell highlight
36429ccd446eSAtari911        //   __text__              → --text-primary        → Primary text, labels, titles
36439ccd446eSAtari911        //   __text_neu__          → --text-dim            → Secondary text, dates, descriptions
36449ccd446eSAtari911        //   __text_alt__          → (not mapped)          → Available for future use
36459ccd446eSAtari911        //   __border__            → --border-color        → Grid lines, input borders
36467e8ea635SAtari911        //                         → --border-main         → Accent color: buttons, badges, active elements, section headers
36479ccd446eSAtari911        //                         → --header-border
36487e8ea635SAtari911        //   __link__              → --text-bright         → Links, accent text
36499ccd446eSAtari911        //   __existing__          → (fallback to __link__)→ Available for future use
36509ccd446eSAtari911        //
36519ccd446eSAtari911        // To customize: edit your template's conf/style.ini [replacements]
36529ccd446eSAtari911        return [
36539ccd446eSAtari911            'bg' => $bgSite,
36547e8ea635SAtari911            'border' => $border,         // Accent color from template border
36559ccd446eSAtari911            'shadow' => 'rgba(0, 0, 0, 0.1)',
36569ccd446eSAtari911            'header_bg' => $bgAlt,       // Headers use alt background
36579ccd446eSAtari911            'header_border' => $border,
36589ccd446eSAtari911            'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
36599ccd446eSAtari911            'text_primary' => $text,
36609ccd446eSAtari911            'text_bright' => $link,
36619ccd446eSAtari911            'text_dim' => $textNeu,
36629ccd446eSAtari911            'grid_bg' => $bgSite,
36639ccd446eSAtari911            'grid_border' => $border,
36649ccd446eSAtari911            'cell_bg' => $background,    // Cells use __background__ (white/light)
36659ccd446eSAtari911            'cell_today_bg' => $bgNeu,
36669ccd446eSAtari911            'bar_glow' => '0 1px 2px',
3667*96df7d3eSAtari911            'pastdue_color' => '#e74c3c',
3668*96df7d3eSAtari911            'pastdue_bg' => '#ffe6e6',
3669*96df7d3eSAtari911            'pastdue_bg_strong' => '#ffd9d9',
3670*96df7d3eSAtari911            'pastdue_bg_light' => '#fff2f2',
3671*96df7d3eSAtari911            'tomorrow_bg' => '#fff9e6',
3672*96df7d3eSAtari911            'tomorrow_bg_strong' => '#fff4cc',
3673*96df7d3eSAtari911            'tomorrow_bg_light' => '#fffbf0',
36749ccd446eSAtari911        ];
36759ccd446eSAtari911    }
36769ccd446eSAtari911
36779ccd446eSAtari911    /**
36789ccd446eSAtari911     * Get theme-specific color styles
36799ccd446eSAtari911     */
36809ccd446eSAtari911    private function getSidebarThemeStyles($theme) {
36819ccd446eSAtari911        // For wiki theme, try to read colors from template's style.ini
36829ccd446eSAtari911        if ($theme === 'wiki') {
36839ccd446eSAtari911            $wikiColors = $this->getWikiTemplateColors();
36849ccd446eSAtari911            if (!empty($wikiColors)) {
36859ccd446eSAtari911                return $wikiColors;
36869ccd446eSAtari911            }
36879ccd446eSAtari911            // Fall through to default wiki colors if reading fails
36889ccd446eSAtari911        }
36899ccd446eSAtari911
36909ccd446eSAtari911        $themes = [
36919ccd446eSAtari911            'matrix' => [
36929ccd446eSAtari911                'bg' => '#242424',
36939ccd446eSAtari911                'border' => '#00cc07',
36949ccd446eSAtari911                'shadow' => 'rgba(0, 204, 7, 0.3)',
36959ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2a2a2a 0%, #242424 100%)',
36969ccd446eSAtari911                'header_border' => '#00cc07',
36979ccd446eSAtari911                'header_shadow' => '0 2px 8px rgba(0, 204, 7, 0.3)',
36989ccd446eSAtari911                'text_primary' => '#00cc07',
36999ccd446eSAtari911                'text_bright' => '#00ff00',
37009ccd446eSAtari911                'text_dim' => '#00aa00',
37019ccd446eSAtari911                'grid_bg' => '#1a3d1a',
37029ccd446eSAtari911                'grid_border' => '#00cc07',
37039ccd446eSAtari911                'cell_bg' => '#242424',
37049ccd446eSAtari911                'cell_today_bg' => '#2a4d2a',
37059ccd446eSAtari911                'bar_glow' => '0 0 3px',
37067e8ea635SAtari911                'pastdue_color' => '#e74c3c',
37077e8ea635SAtari911                'pastdue_bg' => '#3d1a1a',
37087e8ea635SAtari911                'pastdue_bg_strong' => '#4d2020',
37097e8ea635SAtari911                'pastdue_bg_light' => '#2d1515',
37107e8ea635SAtari911                'tomorrow_bg' => '#3d3d1a',
37117e8ea635SAtari911                'tomorrow_bg_strong' => '#4d4d20',
37127e8ea635SAtari911                'tomorrow_bg_light' => '#2d2d15',
37139ccd446eSAtari911            ],
37149ccd446eSAtari911            'purple' => [
37159ccd446eSAtari911                'bg' => '#2a2030',
37169ccd446eSAtari911                'border' => '#9b59b6',
37179ccd446eSAtari911                'shadow' => 'rgba(155, 89, 182, 0.3)',
37189ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2f2438 0%, #2a2030 100%)',
37199ccd446eSAtari911                'header_border' => '#9b59b6',
37209ccd446eSAtari911                'header_shadow' => '0 2px 8px rgba(155, 89, 182, 0.3)',
37219ccd446eSAtari911                'text_primary' => '#b19cd9',
37229ccd446eSAtari911                'text_bright' => '#d4a5ff',
37239ccd446eSAtari911                'text_dim' => '#8e7ab8',
37249ccd446eSAtari911                'grid_bg' => '#3d2b4d',
37259ccd446eSAtari911                'grid_border' => '#9b59b6',
37269ccd446eSAtari911                'cell_bg' => '#2a2030',
37279ccd446eSAtari911                'cell_today_bg' => '#3d2b4d',
37289ccd446eSAtari911                'bar_glow' => '0 0 3px',
37297e8ea635SAtari911                'pastdue_color' => '#e74c3c',
37307e8ea635SAtari911                'pastdue_bg' => '#3d1a2a',
37317e8ea635SAtari911                'pastdue_bg_strong' => '#4d2035',
37327e8ea635SAtari911                'pastdue_bg_light' => '#2d1520',
37337e8ea635SAtari911                'tomorrow_bg' => '#3d3520',
37347e8ea635SAtari911                'tomorrow_bg_strong' => '#4d4028',
37357e8ea635SAtari911                'tomorrow_bg_light' => '#2d2a18',
37369ccd446eSAtari911            ],
37379ccd446eSAtari911            'professional' => [
37389ccd446eSAtari911                'bg' => '#f5f7fa',
37399ccd446eSAtari911                'border' => '#4a90e2',
37409ccd446eSAtari911                'shadow' => 'rgba(74, 144, 226, 0.2)',
37419ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #ffffff 0%, #f5f7fa 100%)',
37429ccd446eSAtari911                'header_border' => '#4a90e2',
37439ccd446eSAtari911                'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
37449ccd446eSAtari911                'text_primary' => '#2c3e50',
37459ccd446eSAtari911                'text_bright' => '#4a90e2',
37469ccd446eSAtari911                'text_dim' => '#7f8c8d',
37479ccd446eSAtari911                'grid_bg' => '#e8ecf1',
37489ccd446eSAtari911                'grid_border' => '#d0d7de',
37499ccd446eSAtari911                'cell_bg' => '#ffffff',
37509ccd446eSAtari911                'cell_today_bg' => '#dce8f7',
37519ccd446eSAtari911                'bar_glow' => '0 1px 2px',
37527e8ea635SAtari911                'pastdue_color' => '#e74c3c',
37537e8ea635SAtari911                'pastdue_bg' => '#ffe6e6',
37547e8ea635SAtari911                'pastdue_bg_strong' => '#ffd9d9',
37557e8ea635SAtari911                'pastdue_bg_light' => '#fff2f2',
37567e8ea635SAtari911                'tomorrow_bg' => '#fff9e6',
37577e8ea635SAtari911                'tomorrow_bg_strong' => '#fff4cc',
37587e8ea635SAtari911                'tomorrow_bg_light' => '#fffbf0',
37599ccd446eSAtari911            ],
37609ccd446eSAtari911            'pink' => [
37619ccd446eSAtari911                'bg' => '#1a0d14',
37629ccd446eSAtari911                'border' => '#ff1493',
37639ccd446eSAtari911                'shadow' => 'rgba(255, 20, 147, 0.4)',
37649ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2d1a24 0%, #1a0d14 100%)',
37659ccd446eSAtari911                'header_border' => '#ff1493',
37669ccd446eSAtari911                'header_shadow' => '0 0 12px rgba(255, 20, 147, 0.6)',
37679ccd446eSAtari911                'text_primary' => '#ff69b4',
37689ccd446eSAtari911                'text_bright' => '#ff1493',
37699ccd446eSAtari911                'text_dim' => '#ff85c1',
37709ccd446eSAtari911                'grid_bg' => '#2d1a24',
37719ccd446eSAtari911                'grid_border' => '#ff1493',
37729ccd446eSAtari911                'cell_bg' => '#1a0d14',
37739ccd446eSAtari911                'cell_today_bg' => '#3d2030',
37749ccd446eSAtari911                'bar_glow' => '0 0 5px',
37757e8ea635SAtari911                'pastdue_color' => '#e74c3c',
37767e8ea635SAtari911                'pastdue_bg' => '#3d1520',
37777e8ea635SAtari911                'pastdue_bg_strong' => '#4d1a28',
37787e8ea635SAtari911                'pastdue_bg_light' => '#2d1018',
37797e8ea635SAtari911                'tomorrow_bg' => '#3d3020',
37807e8ea635SAtari911                'tomorrow_bg_strong' => '#4d3a28',
37817e8ea635SAtari911                'tomorrow_bg_light' => '#2d2518',
37829ccd446eSAtari911            ],
37839ccd446eSAtari911            'wiki' => [
37849ccd446eSAtari911                'bg' => '#f5f5f5',
37857e8ea635SAtari911                'border' => '#ccc',          // Template __border__ color
37869ccd446eSAtari911                'shadow' => 'rgba(0, 0, 0, 0.1)',
37879ccd446eSAtari911                'header_bg' => '#e8e8e8',
37889ccd446eSAtari911                'header_border' => '#ccc',
37899ccd446eSAtari911                'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
37909ccd446eSAtari911                'text_primary' => '#333',
37917e8ea635SAtari911                'text_bright' => '#2b73b7',  // Template __link__ color
37929ccd446eSAtari911                'text_dim' => '#666',
37939ccd446eSAtari911                'grid_bg' => '#f5f5f5',
37949ccd446eSAtari911                'grid_border' => '#ccc',
37959ccd446eSAtari911                'cell_bg' => '#fff',
37969ccd446eSAtari911                'cell_today_bg' => '#eee',
37979ccd446eSAtari911                'bar_glow' => '0 1px 2px',
37987e8ea635SAtari911                'pastdue_color' => '#e74c3c',
37997e8ea635SAtari911                'pastdue_bg' => '#ffe6e6',
38007e8ea635SAtari911                'pastdue_bg_strong' => '#ffd9d9',
38017e8ea635SAtari911                'pastdue_bg_light' => '#fff2f2',
38027e8ea635SAtari911                'tomorrow_bg' => '#fff9e6',
38037e8ea635SAtari911                'tomorrow_bg_strong' => '#fff4cc',
38047e8ea635SAtari911                'tomorrow_bg_light' => '#fffbf0',
38059ccd446eSAtari911            ],
38069ccd446eSAtari911        ];
38079ccd446eSAtari911
38089ccd446eSAtari911        return isset($themes[$theme]) ? $themes[$theme] : $themes['matrix'];
38099ccd446eSAtari911    }
38109ccd446eSAtari911
38119ccd446eSAtari911    /**
38129ccd446eSAtari911     * Get week start day preference
38139ccd446eSAtari911     */
38149ccd446eSAtari911    private function getWeekStartDay() {
38159ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt';
38169ccd446eSAtari911        if (file_exists($configFile)) {
38179ccd446eSAtari911            $start = trim(file_get_contents($configFile));
38189ccd446eSAtari911            if (in_array($start, ['monday', 'sunday'])) {
38199ccd446eSAtari911                return $start;
38209ccd446eSAtari911            }
38219ccd446eSAtari911        }
38229ccd446eSAtari911        return 'sunday'; // Default to Sunday (US/Canada standard)
38239ccd446eSAtari911    }
3824*96df7d3eSAtari911
3825*96df7d3eSAtari911    /**
3826*96df7d3eSAtari911     * Get itinerary collapsed default state
3827*96df7d3eSAtari911     */
3828*96df7d3eSAtari911    private function getItineraryCollapsed() {
3829*96df7d3eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_itinerary_collapsed.txt';
3830*96df7d3eSAtari911        if (file_exists($configFile)) {
3831*96df7d3eSAtari911            return trim(file_get_contents($configFile)) === 'yes';
3832*96df7d3eSAtari911        }
3833*96df7d3eSAtari911        return false; // Default to expanded
3834*96df7d3eSAtari911    }
383519378907SAtari911}