xref: /plugin/calendar/syntax.php (revision da2061786a295965aec1e3343408c72b19fbbb6c)
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' => '',
51*da206178SAtari911            'range' => '',
52*da206178SAtari911            'static' => false,
53*da206178SAtari911            'title' => '',
54*da206178SAtari911            'noprint' => false,
55*da206178SAtari911            'theme' => '',
56*da206178SAtari911            'locked' => false  // Will be set true if month/year specified
5719378907SAtari911        );
5819378907SAtari911
59*da206178SAtari911        // Track if user explicitly set month or year
60*da206178SAtari911        $userSetMonth = false;
61*da206178SAtari911        $userSetYear = false;
62*da206178SAtari911
6319378907SAtari911        if (trim($match)) {
64*da206178SAtari911            // Parse parameters, handling quoted strings properly
65*da206178SAtari911            // Match: key="value with spaces" OR key=value OR standalone_flag
66*da206178SAtari911            preg_match_all('/(\w+)=["\']([^"\']+)["\']|(\w+)=(\S+)|(\w+)/', trim($match), $matches, PREG_SET_ORDER);
67*da206178SAtari911
68*da206178SAtari911            foreach ($matches as $m) {
69*da206178SAtari911                if (!empty($m[1]) && isset($m[2])) {
70*da206178SAtari911                    // key="quoted value"
71*da206178SAtari911                    $key = $m[1];
72*da206178SAtari911                    $value = $m[2];
73*da206178SAtari911                    $params[$key] = $value;
74*da206178SAtari911                    if ($key === 'month') $userSetMonth = true;
75*da206178SAtari911                    if ($key === 'year') $userSetYear = true;
76*da206178SAtari911                } elseif (!empty($m[3]) && isset($m[4])) {
77*da206178SAtari911                    // key=unquoted_value
78*da206178SAtari911                    $key = $m[3];
79*da206178SAtari911                    $value = $m[4];
80*da206178SAtari911                    $params[$key] = $value;
81*da206178SAtari911                    if ($key === 'month') $userSetMonth = true;
82*da206178SAtari911                    if ($key === 'year') $userSetYear = true;
83*da206178SAtari911                } elseif (!empty($m[5])) {
84*da206178SAtari911                    // standalone flag
85*da206178SAtari911                    $params[$m[5]] = true;
8619378907SAtari911                }
8719378907SAtari911            }
8819378907SAtari911        }
8919378907SAtari911
90*da206178SAtari911        // If user explicitly set month or year, lock navigation
91*da206178SAtari911        if ($userSetMonth || $userSetYear) {
92*da206178SAtari911            $params['locked'] = true;
93*da206178SAtari911        }
94*da206178SAtari911
9519378907SAtari911        return $params;
9619378907SAtari911    }
9719378907SAtari911
9819378907SAtari911    public function render($mode, Doku_Renderer $renderer, $data) {
9919378907SAtari911        if ($mode !== 'xhtml') return false;
10019378907SAtari911
1017e8ea635SAtari911        // Disable caching - theme can change via admin without page edit
1027e8ea635SAtari911        $renderer->nocache();
1037e8ea635SAtari911
10419378907SAtari911        if ($data['type'] === 'eventlist') {
10519378907SAtari911            $html = $this->renderStandaloneEventList($data);
10619378907SAtari911        } elseif ($data['type'] === 'eventpanel') {
10719378907SAtari911            $html = $this->renderEventPanelOnly($data);
108*da206178SAtari911        } elseif ($data['static']) {
109*da206178SAtari911            $html = $this->renderStaticCalendar($data);
11019378907SAtari911        } else {
11119378907SAtari911            $html = $this->renderCompactCalendar($data);
11219378907SAtari911        }
11319378907SAtari911
11419378907SAtari911        $renderer->doc .= $html;
11519378907SAtari911        return true;
11619378907SAtari911    }
11719378907SAtari911
11819378907SAtari911    private function renderCompactCalendar($data) {
11919378907SAtari911        $year = (int)$data['year'];
12019378907SAtari911        $month = (int)$data['month'];
12119378907SAtari911        $namespace = $data['namespace'];
12219378907SAtari911
1230c3b6e81SAtari911        // Get theme - prefer inline theme= parameter, fall back to admin default
1240c3b6e81SAtari911        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();
1259ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
1269ccd446eSAtari911        $themeClass = 'calendar-theme-' . $theme;
1279ccd446eSAtari911
1289ccd446eSAtari911        // Determine button text color: professional uses white, others use bg color
1299ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
1309ccd446eSAtari911
131e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
132e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
133e3a9f44cSAtari911
134e3a9f44cSAtari911        if ($isMultiNamespace) {
135e3a9f44cSAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
136e3a9f44cSAtari911        } else {
13719378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
138e3a9f44cSAtari911        }
13919378907SAtari911        $calId = 'cal_' . md5(serialize($data) . microtime());
14019378907SAtari911
14119378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
14219378907SAtari911
14319378907SAtari911        $prevMonth = $month - 1;
14419378907SAtari911        $prevYear = $year;
14519378907SAtari911        if ($prevMonth < 1) {
14619378907SAtari911            $prevMonth = 12;
14719378907SAtari911            $prevYear--;
14819378907SAtari911        }
14919378907SAtari911
15019378907SAtari911        $nextMonth = $month + 1;
15119378907SAtari911        $nextYear = $year;
15219378907SAtari911        if ($nextMonth > 12) {
15319378907SAtari911            $nextMonth = 1;
15419378907SAtari911            $nextYear++;
15519378907SAtari911        }
15619378907SAtari911
15796df7d3eSAtari911        // Get important namespaces from config for highlighting
15896df7d3eSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
15996df7d3eSAtari911        $importantNsList = ['important']; // default
16096df7d3eSAtari911        if (file_exists($configFile)) {
16196df7d3eSAtari911            $config = include $configFile;
16296df7d3eSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
16396df7d3eSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
16496df7d3eSAtari911            }
16596df7d3eSAtari911        }
16696df7d3eSAtari911
1679ccd446eSAtari911        // Container - all styling via CSS variables
16896df7d3eSAtari911        $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)) . '">';
1699ccd446eSAtari911
1709ccd446eSAtari911        // Inject CSS variables for this calendar instance - all theming flows from here
1719ccd446eSAtari911        $html .= '<style>
1729ccd446eSAtari911        #' . $calId . ' {
1739ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
1749ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
1759ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
1769ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
1779ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
1789ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
1799ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
1809ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
1819ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
1829ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
1839ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
1849ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
1859ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
1869ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
1879ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
1887e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
1897e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
1907e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
1917e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
1927e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
1937e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
1947e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
1959ccd446eSAtari911        }
1969ccd446eSAtari911        #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
1979ccd446eSAtari911        #event-search-' . $calId . '::-webkit-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
1989ccd446eSAtari911        #event-search-' . $calId . '::-moz-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
1999ccd446eSAtari911        #event-search-' . $calId . ':-ms-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
2009ccd446eSAtari911        </style>';
2011d05cddcSAtari911
2021d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
2031d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
2041d05cddcSAtari911
2051d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
2061d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
20719378907SAtari911
20819378907SAtari911        // Embed events data as JSON for JavaScript access
20919378907SAtari911        $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>';
21019378907SAtari911
21119378907SAtari911        // Left side: Calendar
21219378907SAtari911        $html .= '<div class="calendar-compact-left">';
21319378907SAtari911
21419378907SAtari911        // Header with navigation
21519378907SAtari911        $html .= '<div class="calendar-compact-header">';
21619378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
217*da206178SAtari911        $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . '</h3>';
21819378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
219*da206178SAtari911        $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
22019378907SAtari911        $html .= '</div>';
22119378907SAtari911
2220c3b6e81SAtari911        // Calendar grid - day name headers as a separate div (avoids Firefox th height issues)
2230c3b6e81SAtari911        $html .= '<div class="calendar-day-headers">';
2240c3b6e81SAtari911        $html .= '<span>S</span><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span>';
2250c3b6e81SAtari911        $html .= '</div>';
22619378907SAtari911        $html .= '<table class="calendar-compact-grid">';
2270c3b6e81SAtari911        $html .= '<tbody>';
22819378907SAtari911
22919378907SAtari911        $firstDay = mktime(0, 0, 0, $month, 1, $year);
23019378907SAtari911        $daysInMonth = date('t', $firstDay);
23119378907SAtari911        $dayOfWeek = date('w', $firstDay);
23219378907SAtari911
233e3a9f44cSAtari911        // Build a map of all events with their date ranges for the calendar grid
23487ac9bf3SAtari911        $eventRanges = array();
235e3a9f44cSAtari911        foreach ($events as $dateKey => $dayEvents) {
23687ac9bf3SAtari911            foreach ($dayEvents as $evt) {
23787ac9bf3SAtari911                $eventId = isset($evt['id']) ? $evt['id'] : '';
23887ac9bf3SAtari911                $startDate = $dateKey;
23987ac9bf3SAtari911                $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey;
24087ac9bf3SAtari911
24187ac9bf3SAtari911                // Only process events that touch this month
24287ac9bf3SAtari911                $eventStart = new DateTime($startDate);
24387ac9bf3SAtari911                $eventEnd = new DateTime($endDate);
24487ac9bf3SAtari911                $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month));
24587ac9bf3SAtari911                $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth));
24687ac9bf3SAtari911
24787ac9bf3SAtari911                // Skip if event doesn't overlap with current month
24887ac9bf3SAtari911                if ($eventEnd < $monthStart || $eventStart > $monthEnd) {
24987ac9bf3SAtari911                    continue;
25087ac9bf3SAtari911                }
25187ac9bf3SAtari911
25287ac9bf3SAtari911                // Create entry for each day the event spans
25387ac9bf3SAtari911                $current = clone $eventStart;
25487ac9bf3SAtari911                while ($current <= $eventEnd) {
25587ac9bf3SAtari911                    $currentKey = $current->format('Y-m-d');
25687ac9bf3SAtari911
25787ac9bf3SAtari911                    // Check if this date is in current month
25887ac9bf3SAtari911                    $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey);
25987ac9bf3SAtari911                    if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) {
26087ac9bf3SAtari911                        if (!isset($eventRanges[$currentKey])) {
26187ac9bf3SAtari911                            $eventRanges[$currentKey] = array();
26287ac9bf3SAtari911                        }
26387ac9bf3SAtari911
26487ac9bf3SAtari911                        // Add event with span information
26587ac9bf3SAtari911                        $evt['_span_start'] = $startDate;
26687ac9bf3SAtari911                        $evt['_span_end'] = $endDate;
26787ac9bf3SAtari911                        $evt['_is_first_day'] = ($currentKey === $startDate);
26887ac9bf3SAtari911                        $evt['_is_last_day'] = ($currentKey === $endDate);
26987ac9bf3SAtari911                        $evt['_original_date'] = $dateKey; // Keep track of original date
27087ac9bf3SAtari911
27187ac9bf3SAtari911                        // Check if event continues from previous month or to next month
27287ac9bf3SAtari911                        $evt['_continues_from_prev'] = ($eventStart < $monthStart);
27387ac9bf3SAtari911                        $evt['_continues_to_next'] = ($eventEnd > $monthEnd);
27487ac9bf3SAtari911
27587ac9bf3SAtari911                        $eventRanges[$currentKey][] = $evt;
27687ac9bf3SAtari911                    }
27787ac9bf3SAtari911
27887ac9bf3SAtari911                    $current->modify('+1 day');
27987ac9bf3SAtari911                }
28087ac9bf3SAtari911            }
28187ac9bf3SAtari911        }
28287ac9bf3SAtari911
28319378907SAtari911        $currentDay = 1;
28419378907SAtari911        $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7);
28519378907SAtari911
28619378907SAtari911        for ($row = 0; $row < $rowCount; $row++) {
28719378907SAtari911            $html .= '<tr>';
28819378907SAtari911            for ($col = 0; $col < 7; $col++) {
28919378907SAtari911                if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) {
29019378907SAtari911                    $html .= '<td class="cal-empty"></td>';
29119378907SAtari911                } else {
29219378907SAtari911                    $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay);
29319378907SAtari911                    $isToday = ($dateKey === date('Y-m-d'));
29487ac9bf3SAtari911                    $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]);
29519378907SAtari911
29619378907SAtari911                    $classes = 'cal-day';
29719378907SAtari911                    if ($isToday) $classes .= ' cal-today';
29819378907SAtari911                    if ($hasEvents) $classes .= ' cal-has-events';
29919378907SAtari911
30019378907SAtari911                    $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">';
3019ccd446eSAtari911
3029ccd446eSAtari911                    $dayNumClass = $isToday ? 'day-num day-num-today' : 'day-num';
3039ccd446eSAtari911                    $html .= '<span class="' . $dayNumClass . '">' . $currentDay . '</span>';
30419378907SAtari911
30519378907SAtari911                    if ($hasEvents) {
30619378907SAtari911                        // Sort events by time (no time first, then by time)
30787ac9bf3SAtari911                        $sortedEvents = $eventRanges[$dateKey];
30819378907SAtari911                        usort($sortedEvents, function($a, $b) {
30919378907SAtari911                            $timeA = isset($a['time']) ? $a['time'] : '';
31019378907SAtari911                            $timeB = isset($b['time']) ? $b['time'] : '';
31119378907SAtari911
31219378907SAtari911                            // Events without time go first
31319378907SAtari911                            if (empty($timeA) && !empty($timeB)) return -1;
31419378907SAtari911                            if (!empty($timeA) && empty($timeB)) return 1;
31519378907SAtari911                            if (empty($timeA) && empty($timeB)) return 0;
31619378907SAtari911
31719378907SAtari911                            // Sort by time
31819378907SAtari911                            return strcmp($timeA, $timeB);
31919378907SAtari911                        });
32019378907SAtari911
32119378907SAtari911                        // Show colored stacked bars for each event
32219378907SAtari911                        $html .= '<div class="event-indicators">';
32319378907SAtari911                        foreach ($sortedEvents as $evt) {
32419378907SAtari911                            $eventId = isset($evt['id']) ? $evt['id'] : '';
32519378907SAtari911                            $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db';
32619378907SAtari911                            $eventTime = isset($evt['time']) ? $evt['time'] : '';
327*da206178SAtari911                            $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event';
32887ac9bf3SAtari911                            $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey;
32987ac9bf3SAtari911                            $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true;
33087ac9bf3SAtari911                            $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true;
33119378907SAtari911
33296df7d3eSAtari911                            // Check if this event is from an important namespace
33396df7d3eSAtari911                            $evtNs = isset($evt['namespace']) ? $evt['namespace'] : '';
33496df7d3eSAtari911                            if (!$evtNs && isset($evt['_namespace'])) {
33596df7d3eSAtari911                                $evtNs = $evt['_namespace'];
33696df7d3eSAtari911                            }
33796df7d3eSAtari911                            $isImportantEvent = false;
33896df7d3eSAtari911                            foreach ($importantNsList as $impNs) {
33996df7d3eSAtari911                                if ($evtNs === $impNs || strpos($evtNs, $impNs . ':') === 0) {
34096df7d3eSAtari911                                    $isImportantEvent = true;
34196df7d3eSAtari911                                    break;
34296df7d3eSAtari911                                }
34396df7d3eSAtari911                            }
34496df7d3eSAtari911
34519378907SAtari911                            $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed';
34619378907SAtari911
34787ac9bf3SAtari911                            // Add classes for multi-day spanning
34887ac9bf3SAtari911                            if (!$isFirstDay) $barClass .= ' event-bar-continues';
34987ac9bf3SAtari911                            if (!$isLastDay) $barClass .= ' event-bar-continuing';
35096df7d3eSAtari911                            if ($isImportantEvent) {
35196df7d3eSAtari911                                $barClass .= ' event-bar-important';
35296df7d3eSAtari911                                if ($isFirstDay) {
35396df7d3eSAtari911                                    $barClass .= ' event-bar-has-star';
35496df7d3eSAtari911                                }
35596df7d3eSAtari911                            }
35696df7d3eSAtari911
35796df7d3eSAtari911                            $titlePrefix = $isImportantEvent ? '⭐ ' : '';
35887ac9bf3SAtari911
35919378907SAtari911                            $html .= '<span class="event-bar ' . $barClass . '" ';
36019378907SAtari911                            $html .= 'style="background: ' . $eventColor . ';" ';
36196df7d3eSAtari911                            $html .= 'title="' . $titlePrefix . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" ';
36287ac9bf3SAtari911                            $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">';
36319378907SAtari911                            $html .= '</span>';
36419378907SAtari911                        }
36519378907SAtari911                        $html .= '</div>';
36619378907SAtari911                    }
36719378907SAtari911
36819378907SAtari911                    $html .= '</td>';
36919378907SAtari911                    $currentDay++;
37019378907SAtari911                }
37119378907SAtari911            }
37219378907SAtari911            $html .= '</tr>';
37319378907SAtari911        }
37419378907SAtari911
37519378907SAtari911        $html .= '</tbody></table>';
37619378907SAtari911        $html .= '</div>'; // End calendar-left
37719378907SAtari911
37819378907SAtari911        // Right side: Event list
37919378907SAtari911        $html .= '<div class="calendar-compact-right">';
38019378907SAtari911        $html .= '<div class="event-list-header">';
38119378907SAtari911        $html .= '<div class="event-list-header-content">';
382*da206178SAtari911        $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>';
38319378907SAtari911        if ($namespace) {
38419378907SAtari911            $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>';
38519378907SAtari911        }
38619378907SAtari911        $html .= '</div>';
3871d05cddcSAtari911
3881d05cddcSAtari911        // Search bar in header
3891d05cddcSAtari911        $html .= '<div class="event-search-container-inline">';
390*da206178SAtari911        $html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" placeholder="�� Search..." oninput="filterEvents(\'' . $calId . '\', this.value)">';
3911d05cddcSAtari911        $html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
392*da206178SAtari911        $html .= '<button class="event-search-mode-inline" id="search-mode-' . $calId . '" onclick="toggleSearchMode(\'' . $calId . '\', \'' . $namespace . '\')" title="Search this month only">��</button>';
3931d05cddcSAtari911        $html .= '</div>';
3941d05cddcSAtari911
395*da206178SAtari911        $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
39619378907SAtari911        $html .= '</div>';
39719378907SAtari911
39819378907SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">';
3999ccd446eSAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace, $themeStyles);
40019378907SAtari911        $html .= '</div>';
40119378907SAtari911
40219378907SAtari911        $html .= '</div>'; // End calendar-right
40319378907SAtari911
40419378907SAtari911        // Event dialog
4050c3b6e81SAtari911        $html .= $this->renderEventDialog($calId, $namespace, $theme);
40619378907SAtari911
40787ac9bf3SAtari911        // Month/Year picker dialog (at container level for proper overlay)
4089ccd446eSAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
40987ac9bf3SAtari911
41019378907SAtari911        $html .= '</div>'; // End container
41119378907SAtari911
41219378907SAtari911        return $html;
41319378907SAtari911    }
41419378907SAtari911
415*da206178SAtari911    /**
416*da206178SAtari911     * Render a static/read-only calendar for presentation and printing
417*da206178SAtari911     * No edit buttons, clean layout, print-friendly itinerary
418*da206178SAtari911     */
419*da206178SAtari911    private function renderStaticCalendar($data) {
420*da206178SAtari911        $year = (int)$data['year'];
421*da206178SAtari911        $month = (int)$data['month'];
422*da206178SAtari911        $namespace = isset($data['namespace']) ? $data['namespace'] : '';
423*da206178SAtari911        $customTitle = isset($data['title']) ? $data['title'] : '';
424*da206178SAtari911        $noprint = isset($data['noprint']) && $data['noprint'];
425*da206178SAtari911        $locked = isset($data['locked']) && $data['locked'];
426*da206178SAtari911        $themeOverride = isset($data['theme']) ? $data['theme'] : '';
427*da206178SAtari911
428*da206178SAtari911        // Generate unique ID for this static calendar
429*da206178SAtari911        $calId = 'static-cal-' . substr(md5($namespace . $year . $month . uniqid()), 0, 8);
430*da206178SAtari911
431*da206178SAtari911        // Get theme settings
432*da206178SAtari911        if ($themeOverride && in_array($themeOverride, ['matrix', 'pink', 'purple', 'professional', 'wiki', 'dark', 'light'])) {
433*da206178SAtari911            $theme = $themeOverride;
434*da206178SAtari911        } else {
435*da206178SAtari911            $theme = $this->getSidebarTheme();
436*da206178SAtari911        }
437*da206178SAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
438*da206178SAtari911
439*da206178SAtari911        // Get important namespaces
440*da206178SAtari911        $importantNsList = $this->getImportantNamespaces();
441*da206178SAtari911
442*da206178SAtari911        // Load events - check for multi-namespace or wildcard
443*da206178SAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
444*da206178SAtari911        if ($isMultiNamespace) {
445*da206178SAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
446*da206178SAtari911        } else {
447*da206178SAtari911            $events = $this->loadEvents($namespace, $year, $month);
448*da206178SAtari911        }
449*da206178SAtari911
450*da206178SAtari911        // Month info
451*da206178SAtari911        $firstDay = mktime(0, 0, 0, $month, 1, $year);
452*da206178SAtari911        $daysInMonth = date('t', $firstDay);
453*da206178SAtari911        $startDayOfWeek = (int)date('w', $firstDay);
454*da206178SAtari911        $monthName = date('F', $firstDay);
455*da206178SAtari911
456*da206178SAtari911        // Display title - custom or default month/year
457*da206178SAtari911        $displayTitle = $customTitle ? $customTitle : $monthName . ' ' . $year;
458*da206178SAtari911
459*da206178SAtari911        // Theme class for styling
460*da206178SAtari911        $themeClass = 'static-theme-' . $theme;
461*da206178SAtari911
462*da206178SAtari911        // Build HTML
463*da206178SAtari911        $html = '<div class="calendar-static ' . $themeClass . '" id="' . $calId . '" data-year="' . $year . '" data-month="' . $month . '" data-namespace="' . hsc($namespace) . '" data-locked="' . ($locked ? '1' : '0') . '">';
464*da206178SAtari911
465*da206178SAtari911        // Screen view: Calendar Grid
466*da206178SAtari911        $html .= '<div class="static-screen-view">';
467*da206178SAtari911
468*da206178SAtari911        // Header with navigation (hide nav buttons if locked)
469*da206178SAtari911        $html .= '<div class="static-header">';
470*da206178SAtari911        if (!$locked) {
471*da206178SAtari911            $html .= '<button class="static-nav-btn" onclick="navStaticCalendar(\'' . $calId . '\', -1)" title="' . $this->getLang('previous_month') . '">◀</button>';
472*da206178SAtari911        }
473*da206178SAtari911        $html .= '<h2 class="static-month-title">' . hsc($displayTitle) . '</h2>';
474*da206178SAtari911        if (!$locked) {
475*da206178SAtari911            $html .= '<button class="static-nav-btn" onclick="navStaticCalendar(\'' . $calId . '\', 1)" title="' . $this->getLang('next_month') . '">▶</button>';
476*da206178SAtari911        }
477*da206178SAtari911        if (!$noprint) {
478*da206178SAtari911            $html .= '<button class="static-print-btn" onclick="printStaticCalendar(\'' . $calId . '\')" title="' . $this->getLang('print_calendar') . '">��️</button>';
479*da206178SAtari911        }
480*da206178SAtari911        $html .= '</div>';
481*da206178SAtari911
482*da206178SAtari911        // Calendar grid
483*da206178SAtari911        $html .= '<table class="static-calendar-grid">';
484*da206178SAtari911        $html .= '<thead><tr>';
485*da206178SAtari911        $dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
486*da206178SAtari911        foreach ($dayNames as $day) {
487*da206178SAtari911            $html .= '<th>' . $day . '</th>';
488*da206178SAtari911        }
489*da206178SAtari911        $html .= '</tr></thead>';
490*da206178SAtari911        $html .= '<tbody>';
491*da206178SAtari911
492*da206178SAtari911        $dayCount = 1;
493*da206178SAtari911        $totalCells = $startDayOfWeek + $daysInMonth;
494*da206178SAtari911        $rows = ceil($totalCells / 7);
495*da206178SAtari911
496*da206178SAtari911        for ($row = 0; $row < $rows; $row++) {
497*da206178SAtari911            $html .= '<tr>';
498*da206178SAtari911            for ($col = 0; $col < 7; $col++) {
499*da206178SAtari911                $cellNum = $row * 7 + $col;
500*da206178SAtari911
501*da206178SAtari911                if ($cellNum < $startDayOfWeek || $dayCount > $daysInMonth) {
502*da206178SAtari911                    $html .= '<td class="static-day-empty"></td>';
503*da206178SAtari911                } else {
504*da206178SAtari911                    $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $dayCount);
505*da206178SAtari911                    $dayEvents = isset($events[$dateKey]) ? $events[$dateKey] : [];
506*da206178SAtari911                    $isToday = ($dateKey === date('Y-m-d'));
507*da206178SAtari911                    $isWeekend = ($col === 0 || $col === 6);
508*da206178SAtari911
509*da206178SAtari911                    $cellClass = 'static-day';
510*da206178SAtari911                    if ($isToday) $cellClass .= ' static-day-today';
511*da206178SAtari911                    if ($isWeekend) $cellClass .= ' static-day-weekend';
512*da206178SAtari911                    if (!empty($dayEvents)) $cellClass .= ' static-day-has-events';
513*da206178SAtari911
514*da206178SAtari911                    $html .= '<td class="' . $cellClass . '">';
515*da206178SAtari911                    $html .= '<div class="static-day-number">' . $dayCount . '</div>';
516*da206178SAtari911
517*da206178SAtari911                    if (!empty($dayEvents)) {
518*da206178SAtari911                        $html .= '<div class="static-day-events">';
519*da206178SAtari911                        foreach ($dayEvents as $event) {
520*da206178SAtari911                            $color = isset($event['color']) ? $event['color'] : '#3498db';
521*da206178SAtari911                            $title = hsc($event['title']);
522*da206178SAtari911                            $time = isset($event['time']) && $event['time'] ? $event['time'] : '';
523*da206178SAtari911                            $desc = isset($event['description']) ? $event['description'] : '';
524*da206178SAtari911                            $eventNs = isset($event['namespace']) ? $event['namespace'] : $namespace;
525*da206178SAtari911
526*da206178SAtari911                            // Check if important
527*da206178SAtari911                            $isImportant = false;
528*da206178SAtari911                            foreach ($importantNsList as $impNs) {
529*da206178SAtari911                                if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
530*da206178SAtari911                                    $isImportant = true;
531*da206178SAtari911                                    break;
532*da206178SAtari911                                }
533*da206178SAtari911                            }
534*da206178SAtari911
535*da206178SAtari911                            // Build tooltip - plain text with basic formatting indicators
536*da206178SAtari911                            $tooltipText = $event['title'];
537*da206178SAtari911                            if ($time) {
538*da206178SAtari911                                $tooltipText .= "\n�� " . $this->formatTime12Hour($time);
539*da206178SAtari911                                if (isset($event['endTime']) && $event['endTime']) {
540*da206178SAtari911                                    $tooltipText .= ' - ' . $this->formatTime12Hour($event['endTime']);
541*da206178SAtari911                                }
542*da206178SAtari911                            }
543*da206178SAtari911                            if ($desc) {
544*da206178SAtari911                                // Convert formatting to plain text equivalents
545*da206178SAtari911                                $plainDesc = $desc;
546*da206178SAtari911                                $plainDesc = preg_replace('/\*\*(.+?)\*\*/', '*$1*', $plainDesc);
547*da206178SAtari911                                $plainDesc = preg_replace('/__(.+?)__/', '*$1*', $plainDesc);
548*da206178SAtari911                                $plainDesc = preg_replace('/\/\/(.+?)\/\//', '_$1_', $plainDesc);
549*da206178SAtari911                                $plainDesc = preg_replace('/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/', '$2 ($1)', $plainDesc);
550*da206178SAtari911                                $plainDesc = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1 ($2)', $plainDesc);
551*da206178SAtari911                                $tooltipText .= "\n\n" . $plainDesc;
552*da206178SAtari911                            }
553*da206178SAtari911
554*da206178SAtari911                            $eventClass = 'static-event';
555*da206178SAtari911                            if ($isImportant) $eventClass .= ' static-event-important';
556*da206178SAtari911
557*da206178SAtari911                            $html .= '<div class="' . $eventClass . '" style="border-left-color: ' . $color . ';" title="' . hsc($tooltipText) . '">';
558*da206178SAtari911                            if ($isImportant) {
559*da206178SAtari911                                $html .= '<span class="static-event-star">⭐</span>';
560*da206178SAtari911                            }
561*da206178SAtari911                            if ($time) {
562*da206178SAtari911                                $html .= '<span class="static-event-time">' . $this->formatTime12Hour($time) . '</span> ';
563*da206178SAtari911                            }
564*da206178SAtari911                            $html .= '<span class="static-event-title">' . $title . '</span>';
565*da206178SAtari911                            $html .= '</div>';
566*da206178SAtari911                        }
567*da206178SAtari911                        $html .= '</div>';
568*da206178SAtari911                    }
569*da206178SAtari911
570*da206178SAtari911                    $html .= '</td>';
571*da206178SAtari911                    $dayCount++;
572*da206178SAtari911                }
573*da206178SAtari911            }
574*da206178SAtari911            $html .= '</tr>';
575*da206178SAtari911        }
576*da206178SAtari911
577*da206178SAtari911        $html .= '</tbody></table>';
578*da206178SAtari911        $html .= '</div>'; // End screen view
579*da206178SAtari911
580*da206178SAtari911        // Print view: Itinerary format (skip if noprint)
581*da206178SAtari911        if (!$noprint) {
582*da206178SAtari911            $html .= '<div class="static-print-view">';
583*da206178SAtari911            $html .= '<h2 class="static-print-title">' . hsc($displayTitle) . '</h2>';
584*da206178SAtari911
585*da206178SAtari911            if (!empty($namespace)) {
586*da206178SAtari911                $html .= '<p class="static-print-namespace">' . $this->getLang('calendar_label') . ': ' . hsc($namespace) . '</p>';
587*da206178SAtari911            }
588*da206178SAtari911
589*da206178SAtari911            // Collect all events sorted by date
590*da206178SAtari911            $allEvents = [];
591*da206178SAtari911        foreach ($events as $dateKey => $dayEvents) {
592*da206178SAtari911            foreach ($dayEvents as $event) {
593*da206178SAtari911                $event['_date'] = $dateKey;
594*da206178SAtari911                $allEvents[] = $event;
595*da206178SAtari911            }
596*da206178SAtari911        }
597*da206178SAtari911
598*da206178SAtari911        // Sort by date, then time
599*da206178SAtari911        usort($allEvents, function($a, $b) {
600*da206178SAtari911            $dateCompare = strcmp($a['_date'], $b['_date']);
601*da206178SAtari911            if ($dateCompare !== 0) return $dateCompare;
602*da206178SAtari911            $timeA = isset($a['time']) ? $a['time'] : '99:99';
603*da206178SAtari911            $timeB = isset($b['time']) ? $b['time'] : '99:99';
604*da206178SAtari911            return strcmp($timeA, $timeB);
605*da206178SAtari911        });
606*da206178SAtari911
607*da206178SAtari911        if (empty($allEvents)) {
608*da206178SAtari911            $html .= '<p class="static-print-empty">' . $this->getLang('no_events_scheduled') . '</p>';
609*da206178SAtari911        } else {
610*da206178SAtari911            $html .= '<table class="static-itinerary">';
611*da206178SAtari911            $html .= '<thead><tr><th>Date</th><th>Time</th><th>Event</th><th>Details</th></tr></thead>';
612*da206178SAtari911            $html .= '<tbody>';
613*da206178SAtari911
614*da206178SAtari911            $lastDate = '';
615*da206178SAtari911            foreach ($allEvents as $event) {
616*da206178SAtari911                $dateKey = $event['_date'];
617*da206178SAtari911                $dateObj = new \DateTime($dateKey);
618*da206178SAtari911                $dateDisplay = $dateObj->format('D, M j');
619*da206178SAtari911                $eventNs = isset($event['namespace']) ? $event['namespace'] : $namespace;
620*da206178SAtari911
621*da206178SAtari911                // Check if important
622*da206178SAtari911                $isImportant = false;
623*da206178SAtari911                foreach ($importantNsList as $impNs) {
624*da206178SAtari911                    if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
625*da206178SAtari911                        $isImportant = true;
626*da206178SAtari911                        break;
627*da206178SAtari911                    }
628*da206178SAtari911                }
629*da206178SAtari911
630*da206178SAtari911                $rowClass = $isImportant ? 'static-itinerary-important' : '';
631*da206178SAtari911
632*da206178SAtari911                $html .= '<tr class="' . $rowClass . '">';
633*da206178SAtari911
634*da206178SAtari911                // Only show date if different from previous row
635*da206178SAtari911                if ($dateKey !== $lastDate) {
636*da206178SAtari911                    $html .= '<td class="static-itinerary-date">' . $dateDisplay . '</td>';
637*da206178SAtari911                    $lastDate = $dateKey;
638*da206178SAtari911                } else {
639*da206178SAtari911                    $html .= '<td></td>';
640*da206178SAtari911                }
641*da206178SAtari911
642*da206178SAtari911                // Time
643*da206178SAtari911                $time = isset($event['time']) && $event['time'] ? $this->formatTime12Hour($event['time']) : $this->getLang('all_day');
644*da206178SAtari911                if (isset($event['endTime']) && $event['endTime'] && isset($event['time']) && $event['time']) {
645*da206178SAtari911                    $time .= ' - ' . $this->formatTime12Hour($event['endTime']);
646*da206178SAtari911                }
647*da206178SAtari911                $html .= '<td class="static-itinerary-time">' . $time . '</td>';
648*da206178SAtari911
649*da206178SAtari911                // Title with star for important
650*da206178SAtari911                $html .= '<td class="static-itinerary-title">';
651*da206178SAtari911                if ($isImportant) {
652*da206178SAtari911                    $html .= '⭐ ';
653*da206178SAtari911                }
654*da206178SAtari911                $html .= hsc($event['title']);
655*da206178SAtari911                $html .= '</td>';
656*da206178SAtari911
657*da206178SAtari911                // Description - with formatting
658*da206178SAtari911                $desc = isset($event['description']) ? $this->renderDescription($event['description']) : '';
659*da206178SAtari911                $html .= '<td class="static-itinerary-desc">' . $desc . '</td>';
660*da206178SAtari911
661*da206178SAtari911                $html .= '</tr>';
662*da206178SAtari911            }
663*da206178SAtari911
664*da206178SAtari911            $html .= '</tbody></table>';
665*da206178SAtari911        }
666*da206178SAtari911
667*da206178SAtari911        $html .= '</div>'; // End print view
668*da206178SAtari911        } // End noprint check
669*da206178SAtari911
670*da206178SAtari911        $html .= '</div>'; // End container
671*da206178SAtari911
672*da206178SAtari911        return $html;
673*da206178SAtari911    }
674*da206178SAtari911
675*da206178SAtari911    /**
676*da206178SAtari911     * Format time to 12-hour format
677*da206178SAtari911     */
678*da206178SAtari911    private function formatTime12Hour($time) {
679*da206178SAtari911        if (!$time) return '';
680*da206178SAtari911        $parts = explode(':', $time);
681*da206178SAtari911        $hour = (int)$parts[0];
682*da206178SAtari911        $minute = isset($parts[1]) ? $parts[1] : '00';
683*da206178SAtari911        $ampm = $hour >= 12 ? 'PM' : 'AM';
684*da206178SAtari911        $hour12 = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
685*da206178SAtari911        return $hour12 . ':' . $minute . ' ' . $ampm;
686*da206178SAtari911    }
687*da206178SAtari911
688*da206178SAtari911    /**
689*da206178SAtari911     * Get list of important namespaces from config
690*da206178SAtari911     */
691*da206178SAtari911    private function getImportantNamespaces() {
692*da206178SAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
693*da206178SAtari911        $importantNsList = ['important']; // default
694*da206178SAtari911        if (file_exists($configFile)) {
695*da206178SAtari911            $config = include $configFile;
696*da206178SAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
697*da206178SAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
698*da206178SAtari911            }
699*da206178SAtari911        }
700*da206178SAtari911        return $importantNsList;
701*da206178SAtari911    }
702*da206178SAtari911
7039ccd446eSAtari911    private function renderEventListContent($events, $calId, $namespace, $themeStyles = null) {
70419378907SAtari911        if (empty($events)) {
70519378907SAtari911            return '<p class="no-events-msg">No events this month</p>';
70619378907SAtari911        }
70719378907SAtari911
7089ccd446eSAtari911        // Default theme styles if not provided
7099ccd446eSAtari911        if ($themeStyles === null) {
7109ccd446eSAtari911            $theme = $this->getSidebarTheme();
7119ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
71296df7d3eSAtari911        } else {
71396df7d3eSAtari911            $theme = $this->getSidebarTheme();
71496df7d3eSAtari911        }
71596df7d3eSAtari911
71696df7d3eSAtari911        // Get important namespaces from config
71796df7d3eSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
71896df7d3eSAtari911        $importantNsList = ['important']; // default
71996df7d3eSAtari911        if (file_exists($configFile)) {
72096df7d3eSAtari911            $config = include $configFile;
72196df7d3eSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
72296df7d3eSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
72396df7d3eSAtari911            }
7249ccd446eSAtari911        }
7259ccd446eSAtari911
7261d05cddcSAtari911        // Check for time conflicts
7271d05cddcSAtari911        $events = $this->checkTimeConflicts($events);
7281d05cddcSAtari911
729e3a9f44cSAtari911        // Sort by date ascending (chronological order - oldest first)
73019378907SAtari911        ksort($events);
73119378907SAtari911
732e3a9f44cSAtari911        // Sort events within each day by time
733e3a9f44cSAtari911        foreach ($events as $dateKey => &$dayEvents) {
734e3a9f44cSAtari911            usort($dayEvents, function($a, $b) {
7351d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
7361d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
7371d05cddcSAtari911
7381d05cddcSAtari911                // All-day events (no time) go to the TOP
7391d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
7401d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
7411d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
7421d05cddcSAtari911
7431d05cddcSAtari911                // Both have times, sort chronologically
744e3a9f44cSAtari911                return strcmp($timeA, $timeB);
745e3a9f44cSAtari911            });
746e3a9f44cSAtari911        }
747e3a9f44cSAtari911        unset($dayEvents); // Break reference
748e3a9f44cSAtari911
749e3a9f44cSAtari911        // Get today's date for comparison
750e3a9f44cSAtari911        $today = date('Y-m-d');
751e3a9f44cSAtari911        $firstFutureEventId = null;
752e3a9f44cSAtari911
7531d05cddcSAtari911        // Helper function to check if event is past (with 15-minute grace period for timed events)
7541d05cddcSAtari911        $isEventPast = function($dateKey, $time) use ($today) {
7551d05cddcSAtari911            // If event is on a past date, it's definitely past
7561d05cddcSAtari911            if ($dateKey < $today) {
7571d05cddcSAtari911                return true;
7581d05cddcSAtari911            }
7591d05cddcSAtari911
7601d05cddcSAtari911            // If event is on a future date, it's definitely not past
7611d05cddcSAtari911            if ($dateKey > $today) {
7621d05cddcSAtari911                return false;
7631d05cddcSAtari911            }
7641d05cddcSAtari911
7651d05cddcSAtari911            // Event is today - check time with grace period
7661d05cddcSAtari911            if ($time && $time !== '') {
7671d05cddcSAtari911                try {
7681d05cddcSAtari911                    $currentDateTime = new DateTime();
7691d05cddcSAtari911                    $eventDateTime = new DateTime($dateKey . ' ' . $time);
7701d05cddcSAtari911
7711d05cddcSAtari911                    // Add 15-minute grace period
7721d05cddcSAtari911                    $eventDateTime->modify('+15 minutes');
7731d05cddcSAtari911
7741d05cddcSAtari911                    // Event is past if current time > event time + 15 minutes
7751d05cddcSAtari911                    return $currentDateTime > $eventDateTime;
7761d05cddcSAtari911                } catch (Exception $e) {
7771d05cddcSAtari911                    // If time parsing fails, fall back to date-only comparison
7781d05cddcSAtari911                    return false;
7791d05cddcSAtari911                }
7801d05cddcSAtari911            }
7811d05cddcSAtari911
7821d05cddcSAtari911            // No time specified for today's event, treat as future
7831d05cddcSAtari911            return false;
7841d05cddcSAtari911        };
7851d05cddcSAtari911
7861d05cddcSAtari911        // Build HTML for each event - separate past/completed from future
7871d05cddcSAtari911        $pastHtml = '';
7881d05cddcSAtari911        $futureHtml = '';
7891d05cddcSAtari911        $pastCount = 0;
790e3a9f44cSAtari911
79119378907SAtari911        foreach ($events as $dateKey => $dayEvents) {
792e3a9f44cSAtari911
79319378907SAtari911            foreach ($dayEvents as $event) {
794e3a9f44cSAtari911                // Track first future/today event for auto-scroll
795e3a9f44cSAtari911                if (!$firstFutureEventId && $dateKey >= $today) {
796e3a9f44cSAtari911                    $firstFutureEventId = isset($event['id']) ? $event['id'] : '';
797e3a9f44cSAtari911                }
79819378907SAtari911                $eventId = isset($event['id']) ? $event['id'] : '';
79919378907SAtari911                $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
8001d05cddcSAtari911                $timeRaw = isset($event['time']) ? $event['time'] : '';
8011d05cddcSAtari911                $time = htmlspecialchars($timeRaw);
8021d05cddcSAtari911                $endTime = isset($event['endTime']) ? htmlspecialchars($event['endTime']) : '';
80319378907SAtari911                $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db';
80419378907SAtari911                $description = isset($event['description']) ? $event['description'] : '';
80519378907SAtari911                $isTask = isset($event['isTask']) ? $event['isTask'] : false;
80619378907SAtari911                $completed = isset($event['completed']) ? $event['completed'] : false;
80719378907SAtari911                $endDate = isset($event['endDate']) ? $event['endDate'] : '';
80819378907SAtari911
8091d05cddcSAtari911                // Use helper function to determine if event is past (with grace period)
8101d05cddcSAtari911                $isPast = $isEventPast($dateKey, $timeRaw);
8111d05cddcSAtari911                $isToday = $dateKey === $today;
8121d05cddcSAtari911
8131d05cddcSAtari911                // Check if event should be in past section
8141d05cddcSAtari911                // EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past
8151d05cddcSAtari911                $isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed;
8161d05cddcSAtari911                if ($isPastOrCompleted) {
8171d05cddcSAtari911                    $pastCount++;
8181d05cddcSAtari911                }
8191d05cddcSAtari911
8201d05cddcSAtari911                // Determine if task is past due (past date, is task, not completed)
8211d05cddcSAtari911                $isPastDue = $isPast && $isTask && !$completed;
8221d05cddcSAtari911
82319378907SAtari911                // Process description for wiki syntax, HTML, images, and links
8249ccd446eSAtari911                $renderedDescription = $this->renderDescription($description, $themeStyles);
82519378907SAtari911
8261d05cddcSAtari911                // Convert to 12-hour format and handle time ranges
82719378907SAtari911                $displayTime = '';
82819378907SAtari911                if ($time) {
82919378907SAtari911                    $timeObj = DateTime::createFromFormat('H:i', $time);
83019378907SAtari911                    if ($timeObj) {
83119378907SAtari911                        $displayTime = $timeObj->format('g:i A');
8321d05cddcSAtari911
8331d05cddcSAtari911                        // Add end time if present and different from start time
8341d05cddcSAtari911                        if ($endTime && $endTime !== $time) {
8351d05cddcSAtari911                            $endTimeObj = DateTime::createFromFormat('H:i', $endTime);
8361d05cddcSAtari911                            if ($endTimeObj) {
8371d05cddcSAtari911                                $displayTime .= ' - ' . $endTimeObj->format('g:i A');
8381d05cddcSAtari911                            }
8391d05cddcSAtari911                        }
84019378907SAtari911                    } else {
84119378907SAtari911                        $displayTime = $time;
84219378907SAtari911                    }
84319378907SAtari911                }
84419378907SAtari911
84587ac9bf3SAtari911                // Format date display with day of week
846e3a9f44cSAtari911                // Use originalStartDate if this is a multi-month event continuation
847e3a9f44cSAtari911                $displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey;
848e3a9f44cSAtari911                $dateObj = new DateTime($displayDateKey);
84987ac9bf3SAtari911                $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24"
85019378907SAtari911
85119378907SAtari911                // Multi-day indicator
85219378907SAtari911                $multiDay = '';
853e3a9f44cSAtari911                if ($endDate && $endDate !== $displayDateKey) {
85419378907SAtari911                    $endObj = new DateTime($endDate);
85587ac9bf3SAtari911                    $multiDay = ' → ' . $endObj->format('D, M j');
85619378907SAtari911                }
85719378907SAtari911
85819378907SAtari911                $completedClass = $completed ? ' event-completed' : '';
8591d05cddcSAtari911                // Don't grey out past due tasks - they need attention!
8601d05cddcSAtari911                $pastClass = ($isPast && !$isPastDue) ? ' event-past' : '';
8611d05cddcSAtari911                $pastDueClass = $isPastDue ? ' event-pastdue' : '';
862e3a9f44cSAtari911                $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : '';
86319378907SAtari911
86496df7d3eSAtari911                // Check if this is an important namespace event
86596df7d3eSAtari911                $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
86696df7d3eSAtari911                if (!$eventNamespace && isset($event['_namespace'])) {
86796df7d3eSAtari911                    $eventNamespace = $event['_namespace'];
86896df7d3eSAtari911                }
86996df7d3eSAtari911                $isImportantNs = false;
87096df7d3eSAtari911                foreach ($importantNsList as $impNs) {
87196df7d3eSAtari911                    if ($eventNamespace === $impNs || strpos($eventNamespace, $impNs . ':') === 0) {
87296df7d3eSAtari911                        $isImportantNs = true;
87396df7d3eSAtari911                        break;
87496df7d3eSAtari911                    }
87596df7d3eSAtari911                }
87696df7d3eSAtari911                $importantClass = $isImportantNs ? ' event-important' : '';
87796df7d3eSAtari911
8789ccd446eSAtari911                // For all themes: use CSS variables, only keep border-left-color as inline
8799ccd446eSAtari911                $pastClickHandler = ($isPast && !$isPastDue) ? ' onclick="togglePastEventExpand(this)"' : '';
88096df7d3eSAtari911                $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 . '>';
8811d05cddcSAtari911                $eventHtml .= '<div class="event-info">';
8829ccd446eSAtari911
8831d05cddcSAtari911                $eventHtml .= '<div class="event-title-row">';
88496df7d3eSAtari911                // Add star for important namespace events
88596df7d3eSAtari911                if ($isImportantNs) {
886*da206178SAtari911                    $eventHtml .= '<span class="event-important-star" title="Important">⭐</span> ';
88796df7d3eSAtari911                }
8881d05cddcSAtari911                $eventHtml .= '<span class="event-title-compact">' . $title . '</span>';
8891d05cddcSAtari911                $eventHtml .= '</div>';
89019378907SAtari911
891e3a9f44cSAtari911                // For past events, hide meta and description (collapsed)
8921d05cddcSAtari911                // EXCEPTION: Past due tasks should show their details
8931d05cddcSAtari911                if (!$isPast || $isPastDue) {
8941d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact">';
8951d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
89619378907SAtari911                    if ($displayTime) {
8971d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
89819378907SAtari911                    }
8991d05cddcSAtari911                    // Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks
9001d05cddcSAtari911                    if ($isPastDue) {
9017e8ea635SAtari911                        $eventHtml .= ' <span class="event-pastdue-badge" style="background:' . $themeStyles['pastdue_color'] . ' !important; color:white !important; -webkit-text-fill-color:white !important;">' . 'PAST DUE</span>';
9021d05cddcSAtari911                    } elseif ($isToday) {
9037e8ea635SAtari911                        $eventHtml .= ' <span class="event-today-badge" style="background:' . $themeStyles['border'] . ' !important; color:' . $themeStyles['bg'] . ' !important; -webkit-text-fill-color:' . $themeStyles['bg'] . ' !important;">' . 'TODAY</span>';
904e3a9f44cSAtari911                    }
9051d05cddcSAtari911                    // Add namespace badge - ALWAYS show if event has a namespace
906e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
907e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
908e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility
909e3a9f44cSAtari911                    }
9101d05cddcSAtari911                    // Show badge if namespace exists and is not empty
9111d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
9127e8ea635SAtari911                        $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>';
913e3a9f44cSAtari911                    }
9141d05cddcSAtari911
9151d05cddcSAtari911                    // Add conflict warning if event has time conflicts
9161d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
9171d05cddcSAtari911                        $conflictList = [];
9181d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
9199ccd446eSAtari911                            $conflictText = $conflict['title'];
9201d05cddcSAtari911                            if (!empty($conflict['time'])) {
9211d05cddcSAtari911                                // Format time range
9221d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
9231d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
9241d05cddcSAtari911
9251d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
9261d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
9271d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
9281d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
9291d05cddcSAtari911                                } else {
9301d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
9311d05cddcSAtari911                                }
9321d05cddcSAtari911                            }
9331d05cddcSAtari911                            $conflictList[] = $conflictText;
9341d05cddcSAtari911                        }
9351d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
9369ccd446eSAtari911                        $conflictJson = base64_encode(json_encode($conflictList));
9371d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
9381d05cddcSAtari911                    }
9391d05cddcSAtari911
9401d05cddcSAtari911                    $eventHtml .= '</span>';
9411d05cddcSAtari911                    $eventHtml .= '</div>';
94219378907SAtari911
94319378907SAtari911                    if ($description) {
9441d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>';
9451d05cddcSAtari911                    }
9461d05cddcSAtari911                } else {
9471d05cddcSAtari911                    // Past events: render with display:none for click-to-expand
9481d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact" style="display:none;">';
9491d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
9501d05cddcSAtari911                    if ($displayTime) {
9511d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
9521d05cddcSAtari911                    }
9531d05cddcSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
9541d05cddcSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
9551d05cddcSAtari911                        $eventNamespace = $event['_namespace'];
9561d05cddcSAtari911                    }
9571d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
9587e8ea635SAtari911                        $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>';
9591d05cddcSAtari911                    }
9601d05cddcSAtari911
9611d05cddcSAtari911                    // Add conflict warning if event has time conflicts
9621d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
9631d05cddcSAtari911                        $conflictList = [];
9641d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
9659ccd446eSAtari911                            $conflictText = $conflict['title'];
9661d05cddcSAtari911                            if (!empty($conflict['time'])) {
9671d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
9681d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
9691d05cddcSAtari911
9701d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
9711d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
9721d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
9731d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
9741d05cddcSAtari911                                } else {
9751d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
9761d05cddcSAtari911                                }
9771d05cddcSAtari911                            }
9781d05cddcSAtari911                            $conflictList[] = $conflictText;
9791d05cddcSAtari911                        }
9801d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
9819ccd446eSAtari911                        $conflictJson = base64_encode(json_encode($conflictList));
9821d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
9831d05cddcSAtari911                    }
9841d05cddcSAtari911
9851d05cddcSAtari911                    $eventHtml .= '</span>';
9861d05cddcSAtari911                    $eventHtml .= '</div>';
9871d05cddcSAtari911
9881d05cddcSAtari911                    if ($description) {
9891d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '</div>';
99019378907SAtari911                    }
991e3a9f44cSAtari911                }
99219378907SAtari911
9931d05cddcSAtari911                $eventHtml .= '</div>'; // event-info
99419378907SAtari911
995e3a9f44cSAtari911                // Use stored namespace from event, fallback to passed namespace
996e3a9f44cSAtari911                $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace;
997e3a9f44cSAtari911
9981d05cddcSAtari911                $eventHtml .= '<div class="event-actions-compact">';
9991d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">��️</button>';
10001d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>';
10011d05cddcSAtari911                $eventHtml .= '</div>';
100219378907SAtari911
100319378907SAtari911                // Checkbox for tasks - ON THE FAR RIGHT
100419378907SAtari911                if ($isTask) {
100519378907SAtari911                    $checked = $completed ? 'checked' : '';
10061d05cddcSAtari911                    $eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">';
100719378907SAtari911                }
100819378907SAtari911
10091d05cddcSAtari911                $eventHtml .= '</div>';
10101d05cddcSAtari911
10111d05cddcSAtari911                // Add to appropriate section
10121d05cddcSAtari911                if ($isPastOrCompleted) {
10131d05cddcSAtari911                    $pastHtml .= $eventHtml;
10141d05cddcSAtari911                } else {
10151d05cddcSAtari911                    $futureHtml .= $eventHtml;
10161d05cddcSAtari911                }
10171d05cddcSAtari911            }
10181d05cddcSAtari911        }
10191d05cddcSAtari911
10201d05cddcSAtari911        // Build final HTML with collapsible past events section
10211d05cddcSAtari911        $html = '';
10221d05cddcSAtari911
10231d05cddcSAtari911        // Add collapsible past events section if any exist
10241d05cddcSAtari911        if ($pastCount > 0) {
10251d05cddcSAtari911            $html .= '<div class="past-events-section">';
10261d05cddcSAtari911            $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">';
10271d05cddcSAtari911            $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> ';
10281d05cddcSAtari911            $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>';
102919378907SAtari911            $html .= '</div>';
10301d05cddcSAtari911            $html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">';
10311d05cddcSAtari911            $html .= $pastHtml;
10321d05cddcSAtari911            $html .= '</div>';
10331d05cddcSAtari911            $html .= '</div>';
10341d05cddcSAtari911        }
1035e3a9f44cSAtari911
10361d05cddcSAtari911        // Add future events
10371d05cddcSAtari911        $html .= $futureHtml;
103819378907SAtari911
103919378907SAtari911        return $html;
104019378907SAtari911    }
104119378907SAtari911
10421d05cddcSAtari911    /**
10431d05cddcSAtari911     * Check for time conflicts between events
10441d05cddcSAtari911     */
10451d05cddcSAtari911    private function checkTimeConflicts($events) {
10461d05cddcSAtari911        // Group events by date
10471d05cddcSAtari911        $eventsByDate = [];
10481d05cddcSAtari911        foreach ($events as $date => $dateEvents) {
10491d05cddcSAtari911            if (!is_array($dateEvents)) continue;
10501d05cddcSAtari911
10511d05cddcSAtari911            foreach ($dateEvents as $evt) {
10521d05cddcSAtari911                if (empty($evt['time'])) continue; // Skip all-day events
10531d05cddcSAtari911
10541d05cddcSAtari911                if (!isset($eventsByDate[$date])) {
10551d05cddcSAtari911                    $eventsByDate[$date] = [];
10561d05cddcSAtari911                }
10571d05cddcSAtari911                $eventsByDate[$date][] = $evt;
10581d05cddcSAtari911            }
10591d05cddcSAtari911        }
10601d05cddcSAtari911
10611d05cddcSAtari911        // Check for overlaps on each date
10621d05cddcSAtari911        foreach ($eventsByDate as $date => $dateEvents) {
10631d05cddcSAtari911            for ($i = 0; $i < count($dateEvents); $i++) {
10641d05cddcSAtari911                for ($j = $i + 1; $j < count($dateEvents); $j++) {
10651d05cddcSAtari911                    if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) {
10661d05cddcSAtari911                        // Mark both events as conflicting
10671d05cddcSAtari911                        $dateEvents[$i]['hasConflict'] = true;
10681d05cddcSAtari911                        $dateEvents[$j]['hasConflict'] = true;
10691d05cddcSAtari911
10701d05cddcSAtari911                        // Store conflict info
10711d05cddcSAtari911                        if (!isset($dateEvents[$i]['conflictsWith'])) {
10721d05cddcSAtari911                            $dateEvents[$i]['conflictsWith'] = [];
10731d05cddcSAtari911                        }
10741d05cddcSAtari911                        if (!isset($dateEvents[$j]['conflictsWith'])) {
10751d05cddcSAtari911                            $dateEvents[$j]['conflictsWith'] = [];
10761d05cddcSAtari911                        }
10771d05cddcSAtari911
10781d05cddcSAtari911                        $dateEvents[$i]['conflictsWith'][] = [
10791d05cddcSAtari911                            'id' => $dateEvents[$j]['id'],
10801d05cddcSAtari911                            'title' => $dateEvents[$j]['title'],
10811d05cddcSAtari911                            'time' => $dateEvents[$j]['time'],
10821d05cddcSAtari911                            'endTime' => isset($dateEvents[$j]['endTime']) ? $dateEvents[$j]['endTime'] : ''
10831d05cddcSAtari911                        ];
10841d05cddcSAtari911
10851d05cddcSAtari911                        $dateEvents[$j]['conflictsWith'][] = [
10861d05cddcSAtari911                            'id' => $dateEvents[$i]['id'],
10871d05cddcSAtari911                            'title' => $dateEvents[$i]['title'],
10881d05cddcSAtari911                            'time' => $dateEvents[$i]['time'],
10891d05cddcSAtari911                            'endTime' => isset($dateEvents[$i]['endTime']) ? $dateEvents[$i]['endTime'] : ''
10901d05cddcSAtari911                        ];
10911d05cddcSAtari911                    }
10921d05cddcSAtari911                }
10931d05cddcSAtari911            }
10941d05cddcSAtari911
10951d05cddcSAtari911            // Update the events array with conflict information
10961d05cddcSAtari911            foreach ($events[$date] as &$evt) {
10971d05cddcSAtari911                foreach ($dateEvents as $checkedEvt) {
10981d05cddcSAtari911                    if ($evt['id'] === $checkedEvt['id']) {
10991d05cddcSAtari911                        if (isset($checkedEvt['hasConflict'])) {
11001d05cddcSAtari911                            $evt['hasConflict'] = $checkedEvt['hasConflict'];
11011d05cddcSAtari911                        }
11021d05cddcSAtari911                        if (isset($checkedEvt['conflictsWith'])) {
11031d05cddcSAtari911                            $evt['conflictsWith'] = $checkedEvt['conflictsWith'];
11041d05cddcSAtari911                        }
11051d05cddcSAtari911                        break;
11061d05cddcSAtari911                    }
11071d05cddcSAtari911                }
11081d05cddcSAtari911            }
11091d05cddcSAtari911        }
11101d05cddcSAtari911
11111d05cddcSAtari911        return $events;
11121d05cddcSAtari911    }
11131d05cddcSAtari911
11141d05cddcSAtari911    /**
11151d05cddcSAtari911     * Check if two events overlap in time
11161d05cddcSAtari911     */
11171d05cddcSAtari911    private function eventsOverlap($evt1, $evt2) {
11181d05cddcSAtari911        if (empty($evt1['time']) || empty($evt2['time'])) {
11191d05cddcSAtari911            return false; // All-day events don't conflict
11201d05cddcSAtari911        }
11211d05cddcSAtari911
11221d05cddcSAtari911        $start1 = $evt1['time'];
11231d05cddcSAtari911        $end1 = isset($evt1['endTime']) && !empty($evt1['endTime']) ? $evt1['endTime'] : $evt1['time'];
11241d05cddcSAtari911
11251d05cddcSAtari911        $start2 = $evt2['time'];
11261d05cddcSAtari911        $end2 = isset($evt2['endTime']) && !empty($evt2['endTime']) ? $evt2['endTime'] : $evt2['time'];
11271d05cddcSAtari911
11281d05cddcSAtari911        // Convert to minutes for easier comparison
11291d05cddcSAtari911        $start1Mins = $this->timeToMinutes($start1);
11301d05cddcSAtari911        $end1Mins = $this->timeToMinutes($end1);
11311d05cddcSAtari911        $start2Mins = $this->timeToMinutes($start2);
11321d05cddcSAtari911        $end2Mins = $this->timeToMinutes($end2);
11331d05cddcSAtari911
11341d05cddcSAtari911        // Check for overlap: start1 < end2 AND start2 < end1
11351d05cddcSAtari911        return $start1Mins < $end2Mins && $start2Mins < $end1Mins;
11361d05cddcSAtari911    }
11371d05cddcSAtari911
11381d05cddcSAtari911    /**
11391d05cddcSAtari911     * Convert HH:MM time to minutes since midnight
11401d05cddcSAtari911     */
11411d05cddcSAtari911    private function timeToMinutes($timeStr) {
11421d05cddcSAtari911        $parts = explode(':', $timeStr);
11431d05cddcSAtari911        if (count($parts) !== 2) return 0;
11441d05cddcSAtari911
11451d05cddcSAtari911        return (int)$parts[0] * 60 + (int)$parts[1];
11461d05cddcSAtari911    }
11471d05cddcSAtari911
114819378907SAtari911    private function renderEventPanelOnly($data) {
114919378907SAtari911        $year = (int)$data['year'];
115019378907SAtari911        $month = (int)$data['month'];
115119378907SAtari911        $namespace = $data['namespace'];
115287ac9bf3SAtari911        $height = isset($data['height']) ? $data['height'] : '400px';
115387ac9bf3SAtari911
115487ac9bf3SAtari911        // Validate height format (must be px, em, rem, vh, or %)
115587ac9bf3SAtari911        if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) {
115687ac9bf3SAtari911            $height = '400px'; // Default fallback
115787ac9bf3SAtari911        }
115819378907SAtari911
11590c3b6e81SAtari911        // Get theme - prefer inline theme= parameter, fall back to admin default
11600c3b6e81SAtari911        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();        $themeStyles = $this->getSidebarThemeStyles($theme);
11619ccd446eSAtari911
1162e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
1163e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
1164e3a9f44cSAtari911
1165e3a9f44cSAtari911        if ($isMultiNamespace) {
1166e3a9f44cSAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
1167e3a9f44cSAtari911        } else {
116819378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
1169e3a9f44cSAtari911        }
117019378907SAtari911        $calId = 'panel_' . md5(serialize($data) . microtime());
117119378907SAtari911
117219378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
117319378907SAtari911
117419378907SAtari911        $prevMonth = $month - 1;
117519378907SAtari911        $prevYear = $year;
117619378907SAtari911        if ($prevMonth < 1) {
117719378907SAtari911            $prevMonth = 12;
117819378907SAtari911            $prevYear--;
117919378907SAtari911        }
118019378907SAtari911
118119378907SAtari911        $nextMonth = $month + 1;
118219378907SAtari911        $nextYear = $year;
118319378907SAtari911        if ($nextMonth > 12) {
118419378907SAtari911            $nextMonth = 1;
118519378907SAtari911            $nextYear++;
118619378907SAtari911        }
118719378907SAtari911
11889ccd446eSAtari911        // Determine button text color based on theme
11899ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
11909ccd446eSAtari911
119196df7d3eSAtari911        // Get important namespaces from config for highlighting
119296df7d3eSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
119396df7d3eSAtari911        $importantNsList = ['important']; // default
119496df7d3eSAtari911        if (file_exists($configFile)) {
119596df7d3eSAtari911            $config = include $configFile;
119696df7d3eSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
119796df7d3eSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
119896df7d3eSAtari911            }
119996df7d3eSAtari911        }
120096df7d3eSAtari911
120196df7d3eSAtari911        $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)) . '">';
12029ccd446eSAtari911
12039ccd446eSAtari911        // Inject CSS variables for this panel instance - same as main calendar
12049ccd446eSAtari911        $html .= '<style>
12059ccd446eSAtari911        #' . $calId . ' {
12069ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
12079ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
12089ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
12099ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
12109ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
12119ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
12129ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
12139ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
12149ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
12159ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
12169ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
12179ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
12189ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
12199ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
12209ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
12217e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
12227e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
12237e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
12247e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
12257e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
12267e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
12277e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
12289ccd446eSAtari911        }
12299ccd446eSAtari911        #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
12309ccd446eSAtari911        </style>';
123119378907SAtari911
12321d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
12331d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
12341d05cddcSAtari911
12351d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
12361d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
12371d05cddcSAtari911
12381d05cddcSAtari911        // Compact two-row header designed for ~500px width
12391d05cddcSAtari911        $html .= '<div class="panel-header-compact">';
12401d05cddcSAtari911
12411d05cddcSAtari911        // Row 1: Navigation and title
12421d05cddcSAtari911        $html .= '<div class="panel-header-row-1">';
12431d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
12441d05cddcSAtari911
12451d05cddcSAtari911        // Compact month name (e.g. "Feb 2026" instead of "February 2026 Events")
12461d05cddcSAtari911        $shortMonthName = date('M Y', mktime(0, 0, 0, $month, 1, $year));
12471d05cddcSAtari911        $html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $shortMonthName . '</h3>';
12481d05cddcSAtari911
12491d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
12501d05cddcSAtari911
12511d05cddcSAtari911        // Namespace badge (if applicable)
125287ac9bf3SAtari911        if ($namespace) {
1253e3a9f44cSAtari911            if ($isMultiNamespace) {
1254e3a9f44cSAtari911                if (strpos($namespace, '*') !== false) {
12557e8ea635SAtari911                    $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>';
1256e3a9f44cSAtari911                } else {
1257e3a9f44cSAtari911                    $namespaceList = array_map('trim', explode(';', $namespace));
12581d05cddcSAtari911                    $nsCount = count($namespaceList);
12597e8ea635SAtari911                    $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>';
1260e3a9f44cSAtari911                }
1261e3a9f44cSAtari911            } else {
12621d05cddcSAtari911                $isFiltering = ($namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false);
12631d05cddcSAtari911                if ($isFiltering) {
12647e8ea635SAtari911                    $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>';
12651d05cddcSAtari911                } else {
12667e8ea635SAtari911                    $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>';
126787ac9bf3SAtari911                }
1268e3a9f44cSAtari911            }
12691d05cddcSAtari911        }
12701d05cddcSAtari911
1271*da206178SAtari911        $html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
127219378907SAtari911        $html .= '</div>';
127319378907SAtari911
12741d05cddcSAtari911        // Row 2: Search and add button
12751d05cddcSAtari911        $html .= '<div class="panel-header-row-2">';
12761d05cddcSAtari911        $html .= '<div class="panel-search-box">';
127796df7d3eSAtari911        $html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placeholder="Search this month..." oninput="filterEvents(\'' . $calId . '\', this.value)">';
12781d05cddcSAtari911        $html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
127996df7d3eSAtari911        $html .= '<button class="panel-search-mode" id="search-mode-' . $calId . '" onclick="toggleSearchMode(\'' . $calId . '\', \'' . $namespace . '\')" title="Search this month only">��</button>';
12801d05cddcSAtari911        $html .= '</div>';
12811d05cddcSAtari911        $html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
12821d05cddcSAtari911        $html .= '</div>';
12831d05cddcSAtari911
128419378907SAtari911        $html .= '</div>';
128519378907SAtari911
128687ac9bf3SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">';
128719378907SAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace);
128819378907SAtari911        $html .= '</div>';
128919378907SAtari911
12900c3b6e81SAtari911        $html .= $this->renderEventDialog($calId, $namespace, $theme);
129119378907SAtari911
129287ac9bf3SAtari911        // Month/Year picker for event panel
12939ccd446eSAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
129487ac9bf3SAtari911
129519378907SAtari911        $html .= '</div>';
129619378907SAtari911
129719378907SAtari911        return $html;
129819378907SAtari911    }
129919378907SAtari911
130019378907SAtari911    private function renderStandaloneEventList($data) {
130119378907SAtari911        $namespace = $data['namespace'];
13021d05cddcSAtari911        // If no namespace specified, show all namespaces
13031d05cddcSAtari911        if (empty($namespace)) {
13041d05cddcSAtari911            $namespace = '*';
13051d05cddcSAtari911        }
130619378907SAtari911        $daterange = $data['daterange'];
130719378907SAtari911        $date = $data['date'];
1308e3a9f44cSAtari911        $range = isset($data['range']) ? strtolower($data['range']) : '';
130987ac9bf3SAtari911        $today = isset($data['today']) ? true : false;
1310e3a9f44cSAtari911        $sidebar = isset($data['sidebar']) ? true : false;
13111d05cddcSAtari911        $showchecked = isset($data['showchecked']) ? true : false; // New parameter
13121d05cddcSAtari911        $noheader = isset($data['noheader']) ? true : false; // New parameter to hide header
131319378907SAtari911
1314e3a9f44cSAtari911        // Handle "range" parameter - day, week, or month
1315e3a9f44cSAtari911        if ($range === 'day') {
13161d05cddcSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
131787ac9bf3SAtari911            $endDate = date('Y-m-d');
1318*da206178SAtari911            $headerText = 'Today';
1319e3a9f44cSAtari911        } elseif ($range === 'week') {
13201d05cddcSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
13211d05cddcSAtari911            $endDateTime = new DateTime();
1322e3a9f44cSAtari911            $endDateTime->modify('+7 days');
1323e3a9f44cSAtari911            $endDate = $endDateTime->format('Y-m-d');
1324*da206178SAtari911            $headerText = 'This Week';
1325e3a9f44cSAtari911        } elseif ($range === 'month') {
13261d05cddcSAtari911            $startDate = date('Y-m-01', strtotime('-1 month')); // Include previous month for past due tasks
1327e3a9f44cSAtari911            $endDate = date('Y-m-t'); // Last of current month
13281d05cddcSAtari911            $dt = new DateTime();
1329e3a9f44cSAtari911            $headerText = $dt->format('F Y');
1330e3a9f44cSAtari911        } elseif ($sidebar) {
13311d05cddcSAtari911            // NEW: Sidebar widget - load current week's events
13329ccd446eSAtari911            $weekStartDay = $this->getWeekStartDay(); // Get saved preference
13339ccd446eSAtari911
13349ccd446eSAtari911            if ($weekStartDay === 'monday') {
13359ccd446eSAtari911                // Monday start
13361d05cddcSAtari911                $weekStart = date('Y-m-d', strtotime('monday this week'));
13371d05cddcSAtari911                $weekEnd = date('Y-m-d', strtotime('sunday this week'));
13389ccd446eSAtari911            } else {
13399ccd446eSAtari911                // Sunday start (default - US/Canada standard)
13409ccd446eSAtari911                $today = date('w'); // 0 (Sun) to 6 (Sat)
13419ccd446eSAtari911                if ($today == 0) {
13429ccd446eSAtari911                    // Today is Sunday
13439ccd446eSAtari911                    $weekStart = date('Y-m-d');
13449ccd446eSAtari911                } else {
13459ccd446eSAtari911                    // Monday-Saturday: go back to last Sunday
13469ccd446eSAtari911                    $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
13479ccd446eSAtari911                }
13489ccd446eSAtari911                $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
13499ccd446eSAtari911            }
13501d05cddcSAtari911
13519ccd446eSAtari911            // Load events for the entire week PLUS tomorrow (if tomorrow is outside week)
13529ccd446eSAtari911            // PLUS next 2 weeks for Important events
13531d05cddcSAtari911            $start = new DateTime($weekStart);
13541d05cddcSAtari911            $end = new DateTime($weekEnd);
13559ccd446eSAtari911
13569ccd446eSAtari911            // Check if we need to extend to include tomorrow
13579ccd446eSAtari911            $tomorrowDate = date('Y-m-d', strtotime('+1 day'));
13589ccd446eSAtari911            if ($tomorrowDate > $weekEnd) {
13599ccd446eSAtari911                // Tomorrow is outside the week, extend end date to include it
13609ccd446eSAtari911                $end = new DateTime($tomorrowDate);
13619ccd446eSAtari911            }
13629ccd446eSAtari911
13639ccd446eSAtari911            // Extend 2 weeks into the future for Important events
13649ccd446eSAtari911            $twoWeeksOut = date('Y-m-d', strtotime($weekEnd . ' +14 days'));
13659ccd446eSAtari911            $end = new DateTime($twoWeeksOut);
13669ccd446eSAtari911
13671d05cddcSAtari911            $end->modify('+1 day'); // DatePeriod excludes end date
13681d05cddcSAtari911            $interval = new DateInterval('P1D');
13691d05cddcSAtari911            $period = new DatePeriod($start, $interval, $end);
13701d05cddcSAtari911
13711d05cddcSAtari911            $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
13721d05cddcSAtari911            $allEvents = [];
13731d05cddcSAtari911            $loadedMonths = [];
13741d05cddcSAtari911
13751d05cddcSAtari911            foreach ($period as $dt) {
13761d05cddcSAtari911                $year = (int)$dt->format('Y');
13771d05cddcSAtari911                $month = (int)$dt->format('n');
13781d05cddcSAtari911                $dateKey = $dt->format('Y-m-d');
13791d05cddcSAtari911
13801d05cddcSAtari911                $monthKey = $year . '-' . $month . '-' . $namespace;
13811d05cddcSAtari911
13821d05cddcSAtari911                if (!isset($loadedMonths[$monthKey])) {
13831d05cddcSAtari911                    if ($isMultiNamespace) {
13841d05cddcSAtari911                        $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
13851d05cddcSAtari911                    } else {
13861d05cddcSAtari911                        $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
13871d05cddcSAtari911                    }
13881d05cddcSAtari911                }
13891d05cddcSAtari911
13901d05cddcSAtari911                $monthEvents = $loadedMonths[$monthKey];
13911d05cddcSAtari911
13921d05cddcSAtari911                if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
13931d05cddcSAtari911                    $allEvents[$dateKey] = $monthEvents[$dateKey];
13941d05cddcSAtari911                }
13951d05cddcSAtari911            }
13961d05cddcSAtari911
13971d05cddcSAtari911            // Apply time conflict detection
13981d05cddcSAtari911            $allEvents = $this->checkTimeConflicts($allEvents);
13991d05cddcSAtari911
14001d05cddcSAtari911            $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8);
14011d05cddcSAtari911
14021d05cddcSAtari911            // Render sidebar widget and return immediately
14030c3b6e81SAtari911            $themeOverride = !empty($data['theme']) ? $data['theme'] : null;
14040c3b6e81SAtari911            return $this->renderSidebarWidget($allEvents, $namespace, $calId, $themeOverride);
1405e3a9f44cSAtari911        } elseif ($today) {
1406e3a9f44cSAtari911            $startDate = date('Y-m-d');
1407e3a9f44cSAtari911            $endDate = date('Y-m-d');
1408*da206178SAtari911            $headerText = 'Today';
140987ac9bf3SAtari911        } elseif ($daterange) {
141019378907SAtari911            list($startDate, $endDate) = explode(':', $daterange);
1411e3a9f44cSAtari911            $start = new DateTime($startDate);
1412e3a9f44cSAtari911            $end = new DateTime($endDate);
1413e3a9f44cSAtari911            $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y');
141419378907SAtari911        } elseif ($date) {
141519378907SAtari911            $startDate = $date;
141619378907SAtari911            $endDate = $date;
1417e3a9f44cSAtari911            $dt = new DateTime($date);
1418e3a9f44cSAtari911            $headerText = $dt->format('l, F j, Y');
141919378907SAtari911        } else {
142019378907SAtari911            $startDate = date('Y-m-01');
142119378907SAtari911            $endDate = date('Y-m-t');
1422e3a9f44cSAtari911            $dt = new DateTime($startDate);
1423e3a9f44cSAtari911            $headerText = $dt->format('F Y');
142419378907SAtari911        }
142519378907SAtari911
1426e3a9f44cSAtari911        // Load all events in date range
142719378907SAtari911        $allEvents = array();
142819378907SAtari911        $start = new DateTime($startDate);
142919378907SAtari911        $end = new DateTime($endDate);
143019378907SAtari911        $end->modify('+1 day');
143119378907SAtari911
143219378907SAtari911        $interval = new DateInterval('P1D');
143319378907SAtari911        $period = new DatePeriod($start, $interval, $end);
143419378907SAtari911
1435e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
1436e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
1437e3a9f44cSAtari911
143819378907SAtari911        static $loadedMonths = array();
143919378907SAtari911
144019378907SAtari911        foreach ($period as $dt) {
144119378907SAtari911            $year = (int)$dt->format('Y');
144219378907SAtari911            $month = (int)$dt->format('n');
144319378907SAtari911            $dateKey = $dt->format('Y-m-d');
144419378907SAtari911
1445e3a9f44cSAtari911            $monthKey = $year . '-' . $month . '-' . $namespace;
144619378907SAtari911
144719378907SAtari911            if (!isset($loadedMonths[$monthKey])) {
1448e3a9f44cSAtari911                if ($isMultiNamespace) {
1449e3a9f44cSAtari911                    $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
1450e3a9f44cSAtari911                } else {
145119378907SAtari911                    $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
145219378907SAtari911                }
1453e3a9f44cSAtari911            }
145419378907SAtari911
145519378907SAtari911            $monthEvents = $loadedMonths[$monthKey];
145619378907SAtari911
145719378907SAtari911            if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
145819378907SAtari911                $allEvents[$dateKey] = $monthEvents[$dateKey];
145919378907SAtari911            }
146019378907SAtari911        }
146119378907SAtari911
14621d05cddcSAtari911        // Sort events by date (already sorted by dateKey), then by time within each day
14631d05cddcSAtari911        foreach ($allEvents as $dateKey => &$dayEvents) {
14641d05cddcSAtari911            usort($dayEvents, function($a, $b) {
14651d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
14661d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
14671d05cddcSAtari911
14681d05cddcSAtari911                // All-day events (no time) go to the TOP
14691d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
14701d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
14711d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
14721d05cddcSAtari911
14731d05cddcSAtari911                // Both have times, sort chronologically
14741d05cddcSAtari911                return strcmp($timeA, $timeB);
14751d05cddcSAtari911            });
14761d05cddcSAtari911        }
14771d05cddcSAtari911        unset($dayEvents); // Break reference
14781d05cddcSAtari911
1479e3a9f44cSAtari911        // Simple 2-line display widget
14801d05cddcSAtari911        $calId = 'eventlist_' . uniqid();
14817e8ea635SAtari911        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();
14827e8ea635SAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
14837e8ea635SAtari911        $isDark = in_array($theme, ['matrix', 'purple', 'pink']);
14847e8ea635SAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
14857e8ea635SAtari911
14867e8ea635SAtari911        // Theme class for CSS targeting
14877e8ea635SAtari911        $themeClass = 'eventlist-theme-' . $theme;
14887e8ea635SAtari911
14897e8ea635SAtari911        // Container styling - dark themes get border + glow, light themes get subtle border
14907e8ea635SAtari911        $containerStyle = 'background:' . $themeStyles['bg'] . ' !important;';
14917e8ea635SAtari911        if ($isDark) {
14927e8ea635SAtari911            $containerStyle .= ' border:2px solid ' . $themeStyles['border'] . ';';
14937e8ea635SAtari911            $containerStyle .= ' border-radius:4px;';
14947e8ea635SAtari911            $containerStyle .= ' box-shadow:0 0 10px ' . $themeStyles['shadow'] . ';';
14957e8ea635SAtari911        } else {
14967e8ea635SAtari911            $containerStyle .= ' border:1px solid ' . $themeStyles['grid_border'] . ';';
14977e8ea635SAtari911            $containerStyle .= ' border-radius:4px;';
14987e8ea635SAtari911        }
14997e8ea635SAtari911
15007e8ea635SAtari911        $html = '<div class="eventlist-simple ' . $themeClass . '" id="' . $calId . '" style="' . $containerStyle . '">';
15017e8ea635SAtari911
15027e8ea635SAtari911        // Inject CSS variables for this eventlist instance
15037e8ea635SAtari911        $html .= '<style>
15047e8ea635SAtari911        #' . $calId . ' {
15057e8ea635SAtari911            --background-site: ' . $themeStyles['bg'] . ';
15067e8ea635SAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
15077e8ea635SAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
15087e8ea635SAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
15097e8ea635SAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
15107e8ea635SAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
15117e8ea635SAtari911            --border-main: ' . $themeStyles['border'] . ';
15127e8ea635SAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
15137e8ea635SAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
15147e8ea635SAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
15157e8ea635SAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
15167e8ea635SAtari911            --btn-text: ' . $btnTextColor . ';
15177e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
15187e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
15197e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
15207e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
15217e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
15227e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
15237e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
15247e8ea635SAtari911        }
15257e8ea635SAtari911        </style>';
15261d05cddcSAtari911
15271d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
15281d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
15291d05cddcSAtari911
15301d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
15311d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
15321d05cddcSAtari911
15331d05cddcSAtari911        // Add compact header with date and clock for "today" mode (unless noheader is set)
15341d05cddcSAtari911        if ($today && !empty($allEvents) && !$noheader) {
15351d05cddcSAtari911            $todayDate = new DateTime();
15361d05cddcSAtari911            $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026"
15371d05cddcSAtari911            $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM"
15381d05cddcSAtari911
15391d05cddcSAtari911            $html .= '<div class="eventlist-today-header">';
15401d05cddcSAtari911            $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>';
15411d05cddcSAtari911            $html .= '<div class="eventlist-bottom-info">';
15421d05cddcSAtari911            $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '">--°</span></span>';
15431d05cddcSAtari911            $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>';
15441d05cddcSAtari911            $html .= '</div>';
15451d05cddcSAtari911
15461d05cddcSAtari911            // Three CPU/Memory bars (all update live)
15471d05cddcSAtari911            $html .= '<div class="eventlist-stats-container">';
15481d05cddcSAtari911
15491d05cddcSAtari911            // 5-minute load average (green, updates every 2 seconds)
15507e8ea635SAtari911            $html .= '<div class="eventlist-cpu-bar" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $calId . '(\'green\')" onmouseout="hideTooltip_' . $calId . '(\'green\')">';
15517e8ea635SAtari911            $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_bright'] . ' !important;"></div>';
15521d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
15531d05cddcSAtari911            $html .= '</div>';
15541d05cddcSAtari911
15551d05cddcSAtari911            // Real-time CPU (purple, updates with 5-sec average)
15567e8ea635SAtari911            $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $calId . '(\'purple\')" onmouseout="hideTooltip_' . $calId . '(\'purple\')">';
15577e8ea635SAtari911            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['border'] . ' !important;"></div>';
15581d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
15591d05cddcSAtari911            $html .= '</div>';
15601d05cddcSAtari911
15611d05cddcSAtari911            // Real-time Memory (orange, updates)
15627e8ea635SAtari911            $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $calId . '(\'orange\')" onmouseout="hideTooltip_' . $calId . '(\'orange\')">';
15637e8ea635SAtari911            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_primary'] . ' !important;"></div>';
15641d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
15651d05cddcSAtari911            $html .= '</div>';
15661d05cddcSAtari911
15671d05cddcSAtari911            $html .= '</div>';
15681d05cddcSAtari911            $html .= '</div>';
15691d05cddcSAtari911
15701d05cddcSAtari911            // Add JavaScript to update clock and weather
15711d05cddcSAtari911            $html .= '<script>
15721d05cddcSAtari911(function() {
15731d05cddcSAtari911    // Update clock every second
15741d05cddcSAtari911    function updateClock() {
15751d05cddcSAtari911        const now = new Date();
15761d05cddcSAtari911        let hours = now.getHours();
15771d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
15781d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
15791d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
15801d05cddcSAtari911        hours = hours % 12 || 12;
15811d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
15821d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
15831d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
15841d05cddcSAtari911    }
15851d05cddcSAtari911    setInterval(updateClock, 1000);
15861d05cddcSAtari911
158796df7d3eSAtari911    // Fetch weather - uses default location, click weather to get local
158896df7d3eSAtari911    var userLocationGranted = false;
158996df7d3eSAtari911    var userLat = 38.5816;  // Sacramento default
159096df7d3eSAtari911    var userLon = -121.4944;
15911d05cddcSAtari911
159296df7d3eSAtari911    function fetchWeatherData(lat, lon) {
159396df7d3eSAtari911        fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_weather=true&temperature_unit=fahrenheit")
15941d05cddcSAtari911            .then(response => response.json())
15951d05cddcSAtari911            .then(data => {
15961d05cddcSAtari911                if (data.current_weather) {
15971d05cddcSAtari911                    const temp = Math.round(data.current_weather.temperature);
15981d05cddcSAtari911                    const weatherCode = data.current_weather.weathercode;
15991d05cddcSAtari911                    const icon = getWeatherIcon(weatherCode);
16001d05cddcSAtari911                    const iconEl = document.getElementById("weather-icon-' . $calId . '");
16011d05cddcSAtari911                    const tempEl = document.getElementById("weather-temp-' . $calId . '");
16021d05cddcSAtari911                    if (iconEl) iconEl.textContent = icon;
16031d05cddcSAtari911                    if (tempEl) tempEl.innerHTML = temp + "&deg;";
16041d05cddcSAtari911                }
16051d05cddcSAtari911            })
16061d05cddcSAtari911            .catch(error => {
16071d05cddcSAtari911                console.log("Weather fetch error:", error);
16081d05cddcSAtari911            });
160996df7d3eSAtari911    }
161096df7d3eSAtari911
161196df7d3eSAtari911    function updateWeather() {
161296df7d3eSAtari911        fetchWeatherData(userLat, userLon);
161396df7d3eSAtari911    }
161496df7d3eSAtari911
161596df7d3eSAtari911    // Allow user to click weather to get local weather (requires user gesture)
161696df7d3eSAtari911    function requestLocalWeather() {
161796df7d3eSAtari911        if (userLocationGranted) return; // Already have permission
161896df7d3eSAtari911
161996df7d3eSAtari911        if ("geolocation" in navigator) {
162096df7d3eSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
162196df7d3eSAtari911                userLat = position.coords.latitude;
162296df7d3eSAtari911                userLon = position.coords.longitude;
162396df7d3eSAtari911                userLocationGranted = true;
162496df7d3eSAtari911                fetchWeatherData(userLat, userLon);
16251d05cddcSAtari911            }, function(error) {
162696df7d3eSAtari911                console.log("Geolocation denied or unavailable, using default location");
16271d05cddcSAtari911            });
16281d05cddcSAtari911        }
16291d05cddcSAtari911    }
16301d05cddcSAtari911
163196df7d3eSAtari911    // Add click handler to weather widget for local weather
163296df7d3eSAtari911    setTimeout(function() {
163396df7d3eSAtari911        var weatherEl = document.querySelector("#weather-icon-' . $calId . '");
163496df7d3eSAtari911        if (weatherEl) {
163596df7d3eSAtari911            weatherEl.style.cursor = "pointer";
163696df7d3eSAtari911            weatherEl.title = "Click for local weather";
163796df7d3eSAtari911            weatherEl.addEventListener("click", requestLocalWeather);
163896df7d3eSAtari911        }
163996df7d3eSAtari911    }, 100);
164096df7d3eSAtari911
16411d05cddcSAtari911    // WMO Weather interpretation codes
16421d05cddcSAtari911    function getWeatherIcon(code) {
16431d05cddcSAtari911        const icons = {
16441d05cddcSAtari911            0: "☀️",   // Clear sky
16451d05cddcSAtari911            1: "��️",   // Mainly clear
16461d05cddcSAtari911            2: "⛅",   // Partly cloudy
16471d05cddcSAtari911            3: "☁️",   // Overcast
16481d05cddcSAtari911            45: "��️",  // Fog
16491d05cddcSAtari911            48: "��️",  // Depositing rime fog
16501d05cddcSAtari911            51: "��️",  // Light drizzle
16511d05cddcSAtari911            53: "��️",  // Moderate drizzle
16521d05cddcSAtari911            55: "��️",  // Dense drizzle
16531d05cddcSAtari911            61: "��️",  // Slight rain
16541d05cddcSAtari911            63: "��️",  // Moderate rain
16551d05cddcSAtari911            65: "⛈️",  // Heavy rain
16561d05cddcSAtari911            71: "��️",  // Slight snow
16571d05cddcSAtari911            73: "��️",  // Moderate snow
16581d05cddcSAtari911            75: "❄️",  // Heavy snow
16591d05cddcSAtari911            77: "��️",  // Snow grains
16601d05cddcSAtari911            80: "��️",  // Slight rain showers
16611d05cddcSAtari911            81: "��️",  // Moderate rain showers
16621d05cddcSAtari911            82: "⛈️",  // Violent rain showers
16631d05cddcSAtari911            85: "��️",  // Slight snow showers
16641d05cddcSAtari911            86: "❄️",  // Heavy snow showers
16651d05cddcSAtari911            95: "⛈️",  // Thunderstorm
16661d05cddcSAtari911            96: "⛈️",  // Thunderstorm with slight hail
16671d05cddcSAtari911            99: "⛈️"   // Thunderstorm with heavy hail
16681d05cddcSAtari911        };
16691d05cddcSAtari911        return icons[code] || "��️";
16701d05cddcSAtari911    }
16711d05cddcSAtari911
16721d05cddcSAtari911    // Update weather immediately and every 10 minutes
16731d05cddcSAtari911    updateWeather();
16741d05cddcSAtari911    setInterval(updateWeather, 600000);
16751d05cddcSAtari911
16761d05cddcSAtari911    // CPU load history for 4-second rolling average
16771d05cddcSAtari911    const cpuHistory = [];
16781d05cddcSAtari911    const CPU_HISTORY_SIZE = 2; // 2 samples × 2 seconds = 4 seconds
16791d05cddcSAtari911
16801d05cddcSAtari911    // Store latest system stats for tooltips
16811d05cddcSAtari911    let latestStats = {
16821d05cddcSAtari911        load: {"1min": 0, "5min": 0, "15min": 0},
16831d05cddcSAtari911        uptime: "",
16841d05cddcSAtari911        memory_details: {},
16851d05cddcSAtari911        top_processes: []
16861d05cddcSAtari911    };
16871d05cddcSAtari911
16881d05cddcSAtari911    // Tooltip functions
16891d05cddcSAtari911    window["showTooltip_' . $calId . '"] = function(color) {
16901d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
16911d05cddcSAtari911        if (!tooltip) {
16921d05cddcSAtari911            console.log("Tooltip element not found for color:", color);
16931d05cddcSAtari911            return;
16941d05cddcSAtari911        }
16951d05cddcSAtari911
16961d05cddcSAtari911
16971d05cddcSAtari911        let content = "";
16981d05cddcSAtari911
16991d05cddcSAtari911        if (color === "green") {
17001d05cddcSAtari911            // Green bar: Load averages and uptime
17011d05cddcSAtari911            content = "<div class=\"tooltip-title\">CPU Load Average</div>";
17021d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
17031d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
17041d05cddcSAtari911            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
17051d05cddcSAtari911            if (latestStats.uptime) {
17067e8ea635SAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_bright'] . ';\">Uptime: " + latestStats.uptime + "</div>";
17071d05cddcSAtari911            }
17087e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['text_bright'] . '", "important");
17097e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['text_bright'] . '", "important");
17107e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_bright'] . '", "important");
17111d05cddcSAtari911        } else if (color === "purple") {
17121d05cddcSAtari911            // Purple bar: Load averages (short-term) and top processes
17131d05cddcSAtari911            content = "<div class=\"tooltip-title\">CPU Load (Short-term)</div>";
17141d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
17151d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
17161d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
17177e8ea635SAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['border'] . ';\" class=\"tooltip-title\">Top Processes</div>";
17181d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
17191d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
17201d05cddcSAtari911                });
17211d05cddcSAtari911            }
17227e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['border'] . '", "important");
17237e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['border'] . '", "important");
17247e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['border'] . '", "important");
17251d05cddcSAtari911        } else if (color === "orange") {
17261d05cddcSAtari911            // Orange bar: Memory details and top processes
17271d05cddcSAtari911            content = "<div class=\"tooltip-title\">Memory Usage</div>";
17281d05cddcSAtari911            if (latestStats.memory_details && latestStats.memory_details.total) {
17291d05cddcSAtari911                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
17301d05cddcSAtari911                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
17311d05cddcSAtari911                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
17321d05cddcSAtari911                if (latestStats.memory_details.cached) {
17331d05cddcSAtari911                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
17341d05cddcSAtari911                }
17351d05cddcSAtari911            } else {
17361d05cddcSAtari911                content += "<div>Loading...</div>";
17371d05cddcSAtari911            }
17381d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
17397e8ea635SAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_primary'] . ';\" class=\"tooltip-title\">Top Processes</div>";
17401d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
17411d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
17421d05cddcSAtari911                });
17431d05cddcSAtari911            }
17447e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['text_primary'] . '", "important");
17457e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['text_primary'] . '", "important");
17467e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_primary'] . '", "important");
17471d05cddcSAtari911        }
17481d05cddcSAtari911
17491d05cddcSAtari911        tooltip.innerHTML = content;
17507e8ea635SAtari911        tooltip.style.setProperty("display", "block");
17517e8ea635SAtari911        tooltip.style.setProperty("background", "' . $themeStyles['bg'] . '", "important");
17521d05cddcSAtari911
17531d05cddcSAtari911        // Position tooltip using fixed positioning above the bar
17541d05cddcSAtari911        const bar = tooltip.parentElement;
17551d05cddcSAtari911        const barRect = bar.getBoundingClientRect();
17561d05cddcSAtari911        const tooltipRect = tooltip.getBoundingClientRect();
17571d05cddcSAtari911
17581d05cddcSAtari911        // Center horizontally on the bar
17591d05cddcSAtari911        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
17601d05cddcSAtari911        // Position above the bar with 8px gap
17611d05cddcSAtari911        const top = barRect.top - tooltipRect.height - 8;
17621d05cddcSAtari911
17631d05cddcSAtari911        tooltip.style.left = left + "px";
17641d05cddcSAtari911        tooltip.style.top = top + "px";
17651d05cddcSAtari911    };
17661d05cddcSAtari911
17671d05cddcSAtari911    window["hideTooltip_' . $calId . '"] = function(color) {
17681d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
17691d05cddcSAtari911        if (tooltip) {
17701d05cddcSAtari911            tooltip.style.display = "none";
17711d05cddcSAtari911        }
17721d05cddcSAtari911    };
17731d05cddcSAtari911
17741d05cddcSAtari911    // Update CPU and memory bars every 2 seconds
17751d05cddcSAtari911    function updateSystemStats() {
17761d05cddcSAtari911        // Fetch real system stats from server
17771d05cddcSAtari911        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
17781d05cddcSAtari911            .then(response => response.json())
17791d05cddcSAtari911            .then(data => {
17801d05cddcSAtari911
17811d05cddcSAtari911                // Store data for tooltips
17821d05cddcSAtari911                latestStats = {
17831d05cddcSAtari911                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
17841d05cddcSAtari911                    uptime: data.uptime || "",
17851d05cddcSAtari911                    memory_details: data.memory_details || {},
17861d05cddcSAtari911                    top_processes: data.top_processes || []
17871d05cddcSAtari911                };
17881d05cddcSAtari911
17891d05cddcSAtari911
17901d05cddcSAtari911                // Update green bar (5-minute average) - updates live now!
17911d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
17921d05cddcSAtari911                if (greenBar) {
17931d05cddcSAtari911                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
17941d05cddcSAtari911                }
17951d05cddcSAtari911
17961d05cddcSAtari911                // Add current CPU to history for purple bar
17971d05cddcSAtari911                cpuHistory.push(data.cpu);
17981d05cddcSAtari911                if (cpuHistory.length > CPU_HISTORY_SIZE) {
17991d05cddcSAtari911                    cpuHistory.shift(); // Remove oldest
18001d05cddcSAtari911                }
18011d05cddcSAtari911
18021d05cddcSAtari911                // Calculate 5-second average for CPU
18031d05cddcSAtari911                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
18041d05cddcSAtari911
18051d05cddcSAtari911                // Update CPU bar (purple) with 5-second average
18061d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
18071d05cddcSAtari911                if (cpuBar) {
18081d05cddcSAtari911                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
18091d05cddcSAtari911                }
18101d05cddcSAtari911
18111d05cddcSAtari911                // Update memory bar (orange) with real data
18121d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
18131d05cddcSAtari911                if (memBar) {
18141d05cddcSAtari911                    memBar.style.width = Math.min(100, data.memory) + "%";
18151d05cddcSAtari911                }
18161d05cddcSAtari911            })
18171d05cddcSAtari911            .catch(error => {
18181d05cddcSAtari911                console.log("System stats error:", error);
18191d05cddcSAtari911                // Fallback to client-side estimates on error
18201d05cddcSAtari911                const cpuFallback = Math.random() * 100;
18211d05cddcSAtari911                cpuHistory.push(cpuFallback);
18221d05cddcSAtari911                if (cpuHistory.length > CPU_HISTORY_SIZE) {
18231d05cddcSAtari911                    cpuHistory.shift();
18241d05cddcSAtari911                }
18251d05cddcSAtari911                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
18261d05cddcSAtari911
18271d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
18281d05cddcSAtari911                if (greenBar) greenBar.style.width = Math.min(100, cpuFallback) + "%";
18291d05cddcSAtari911
18301d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
18311d05cddcSAtari911                if (cpuBar) cpuBar.style.width = Math.min(100, cpuAverage) + "%";
18321d05cddcSAtari911
18331d05cddcSAtari911                let memoryUsage = 0;
18341d05cddcSAtari911                if (performance.memory) {
18351d05cddcSAtari911                    memoryUsage = (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100;
18361d05cddcSAtari911                } else {
18371d05cddcSAtari911                    memoryUsage = Math.random() * 100;
18381d05cddcSAtari911                }
18391d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
18401d05cddcSAtari911                if (memBar) memBar.style.width = Math.min(100, memoryUsage) + "%";
18411d05cddcSAtari911            });
18421d05cddcSAtari911    }
18431d05cddcSAtari911
18441d05cddcSAtari911    // Update immediately and then every 2 seconds
18451d05cddcSAtari911    updateSystemStats();
18461d05cddcSAtari911    setInterval(updateSystemStats, 2000);
18471d05cddcSAtari911})();
18481d05cddcSAtari911</script>';
18491d05cddcSAtari911        }
185019378907SAtari911
185119378907SAtari911        if (empty($allEvents)) {
1852e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-empty">';
1853e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText);
1854e3a9f44cSAtari911            if ($namespace) {
1855e3a9f44cSAtari911                $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>';
185687ac9bf3SAtari911            }
1857e3a9f44cSAtari911            $html .= '</div>';
1858e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-body">No events</div>';
1859e3a9f44cSAtari911            $html .= '</div>';
1860e3a9f44cSAtari911        } else {
1861e3a9f44cSAtari911            // Calculate today and tomorrow's dates for highlighting
18621d05cddcSAtari911            $todayStr = date('Y-m-d');
1863e3a9f44cSAtari911            $tomorrow = date('Y-m-d', strtotime('+1 day'));
1864e3a9f44cSAtari911
1865e3a9f44cSAtari911            foreach ($allEvents as $dateKey => $dayEvents) {
1866e3a9f44cSAtari911                $dateObj = new DateTime($dateKey);
1867e3a9f44cSAtari911                $displayDate = $dateObj->format('D, M j');
1868e3a9f44cSAtari911
18691d05cddcSAtari911                // Check if this date is today or tomorrow or past
1870e3a9f44cSAtari911                // Enable highlighting for sidebar mode AND range modes (day, week, month)
1871e3a9f44cSAtari911                $enableHighlighting = $sidebar || !empty($range);
18721d05cddcSAtari911                $isToday = $enableHighlighting && ($dateKey === $todayStr);
1873e3a9f44cSAtari911                $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow);
18741d05cddcSAtari911                $isPast = $dateKey < $todayStr;
187519378907SAtari911
187619378907SAtari911                foreach ($dayEvents as $event) {
18771d05cddcSAtari911                    // Check if this is a task and if it's completed
18781d05cddcSAtari911                    $isTask = !empty($event['isTask']);
18791d05cddcSAtari911                    $completed = !empty($event['completed']);
18801d05cddcSAtari911
18811d05cddcSAtari911                    // ALWAYS skip completed tasks UNLESS showchecked is explicitly set
18821d05cddcSAtari911                    if (!$showchecked && $isTask && $completed) {
1883e3a9f44cSAtari911                        continue;
1884e3a9f44cSAtari911                    }
188519378907SAtari911
18861d05cddcSAtari911                    // Skip past events that are NOT tasks (only show past due tasks from the past)
18871d05cddcSAtari911                    if ($isPast && !$isTask) {
18881d05cddcSAtari911                        continue;
18891d05cddcSAtari911                    }
18901d05cddcSAtari911
18911d05cddcSAtari911                    // Determine if task is past due (past date, is task, not completed)
18921d05cddcSAtari911                    $isPastDue = $isPast && $isTask && !$completed;
18931d05cddcSAtari911
1894e3a9f44cSAtari911                    // Line 1: Header (Title, Time, Date, Namespace)
1895e3a9f44cSAtari911                    $todayClass = $isToday ? ' eventlist-simple-today' : '';
1896e3a9f44cSAtari911                    $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : '';
18971d05cddcSAtari911                    $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : '';
18981d05cddcSAtari911                    $html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . $pastDueClass . '">';
1899e3a9f44cSAtari911                    $html .= '<div class="eventlist-simple-header">';
1900e3a9f44cSAtari911
1901e3a9f44cSAtari911                    // Title
1902e3a9f44cSAtari911                    $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>';
1903e3a9f44cSAtari911
1904e3a9f44cSAtari911                    // Time (12-hour format)
1905e3a9f44cSAtari911                    if (!empty($event['time'])) {
1906e3a9f44cSAtari911                        $timeParts = explode(':', $event['time']);
190787ac9bf3SAtari911                        if (count($timeParts) === 2) {
190887ac9bf3SAtari911                            $hour = (int)$timeParts[0];
190987ac9bf3SAtari911                            $minute = $timeParts[1];
191087ac9bf3SAtari911                            $ampm = $hour >= 12 ? 'PM' : 'AM';
1911e3a9f44cSAtari911                            $hour = $hour % 12 ?: 12;
191287ac9bf3SAtari911                            $displayTime = $hour . ':' . $minute . ' ' . $ampm;
1913e3a9f44cSAtari911                            $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>';
191419378907SAtari911                        }
191587ac9bf3SAtari911                    }
191687ac9bf3SAtari911
1917e3a9f44cSAtari911                    // Date
1918e3a9f44cSAtari911                    $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>';
1919e3a9f44cSAtari911
19201d05cddcSAtari911                    // Badge: PAST DUE, TODAY, or nothing
19211d05cddcSAtari911                    if ($isPastDue) {
19227e8ea635SAtari911                        $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>';
19231d05cddcSAtari911                    } elseif ($isToday) {
19247e8ea635SAtari911                        $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>';
192587ac9bf3SAtari911                    }
1926e3a9f44cSAtari911
1927e3a9f44cSAtari911                    // Namespace badge (show individual event's namespace)
1928e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
1929e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
1930e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading
193119378907SAtari911                    }
1932e3a9f44cSAtari911                    if ($eventNamespace) {
1933e3a9f44cSAtari911                        $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</span>';
1934e3a9f44cSAtari911                    }
1935e3a9f44cSAtari911
1936e3a9f44cSAtari911                    $html .= '</div>'; // header
1937e3a9f44cSAtari911
1938e3a9f44cSAtari911                    // Line 2: Body (Description only) - only show if description exists
1939e3a9f44cSAtari911                    if (!empty($event['description'])) {
1940e3a9f44cSAtari911                        $html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) . '</div>';
1941e3a9f44cSAtari911                    }
1942e3a9f44cSAtari911
1943e3a9f44cSAtari911                    $html .= '</div>'; // item
194419378907SAtari911                }
194519378907SAtari911            }
194687ac9bf3SAtari911        }
194719378907SAtari911
1948e3a9f44cSAtari911        $html .= '</div>'; // eventlist-simple
194919378907SAtari911
195019378907SAtari911        return $html;
195119378907SAtari911    }
195219378907SAtari911
19530c3b6e81SAtari911    private function renderEventDialog($calId, $namespace, $theme = null) {
19549ccd446eSAtari911        // Get theme for dialog
19550c3b6e81SAtari911        if ($theme === null) {
19569ccd446eSAtari911            $theme = $this->getSidebarTheme();
19570c3b6e81SAtari911        }
19589ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
19599ccd446eSAtari911
196019378907SAtari911        $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">';
196119378907SAtari911        $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>';
196219378907SAtari911
19639ccd446eSAtari911        // Draggable dialog with theme
196419378907SAtari911        $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">';
196519378907SAtari911
196619378907SAtari911        // Header with drag handle and close button
196719378907SAtari911        $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">';
1968*da206178SAtari911        $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>';
196919378907SAtari911        $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>';
197019378907SAtari911        $html .= '</div>';
197119378907SAtari911
197219378907SAtari911        // Form content
197319378907SAtari911        $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">';
197419378907SAtari911
197519378907SAtari911        // Hidden ID field
197619378907SAtari911        $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">';
197719378907SAtari911
19781d05cddcSAtari911        // 1. TITLE
19791d05cddcSAtari911        $html .= '<div class="form-field">';
1980*da206178SAtari911        $html .= '<label class="field-label">�� Title</label>';
1981*da206178SAtari911        $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek input-compact" placeholder="Event or task title...">';
198219378907SAtari911        $html .= '</div>';
198319378907SAtari911
19841d05cddcSAtari911        // 1.5 NAMESPACE SELECTOR (Searchable with fuzzy matching)
19851d05cddcSAtari911        $html .= '<div class="form-field">';
1986*da206178SAtari911        $html .= '<label class="field-label">�� Namespace</label>';
19871d05cddcSAtari911
19881d05cddcSAtari911        // Hidden field to store actual selected namespace
19891d05cddcSAtari911        $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">';
19901d05cddcSAtari911
19911d05cddcSAtari911        // Searchable input
19921d05cddcSAtari911        $html .= '<div class="namespace-search-wrapper">';
1993*da206178SAtari911        $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">';
19941d05cddcSAtari911        $html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="display:none;"></div>';
19951d05cddcSAtari911        $html .= '</div>';
19961d05cddcSAtari911
19971d05cddcSAtari911        // Store namespaces as JSON for JavaScript
19981d05cddcSAtari911        $allNamespaces = $this->getAllNamespaces();
19991d05cddcSAtari911        $html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($allNamespaces) . '</script>';
20001d05cddcSAtari911
20011d05cddcSAtari911        $html .= '</div>';
20021d05cddcSAtari911
20031d05cddcSAtari911        // 2. DESCRIPTION
20041d05cddcSAtari911        $html .= '<div class="form-field">';
2005*da206178SAtari911        $html .= '<label class="field-label">�� Description</label>';
2006*da206178SAtari911        $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="2" class="input-sleek textarea-sleek textarea-compact" placeholder="Optional details..."></textarea>';
20071d05cddcSAtari911        $html .= '</div>';
20081d05cddcSAtari911
20091d05cddcSAtari911        // 3. START DATE - END DATE (inline)
201019378907SAtari911        $html .= '<div class="form-row-group">';
201119378907SAtari911
20121d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
2013*da206178SAtari911        $html .= '<label class="field-label-compact">�� Start Date</label>';
2014*da206178SAtari911        $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date input-compact" onchange="updateEndTimeOptions(\'' . $calId . '\')">';
201519378907SAtari911        $html .= '</div>';
201619378907SAtari911
20171d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
2018*da206178SAtari911        $html .= '<label class="field-label-compact">�� End Date</label>';
2019*da206178SAtari911        $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date input-compact" placeholder="Optional" onchange="updateEndTimeOptions(\'' . $calId . '\')">';
202019378907SAtari911        $html .= '</div>';
202119378907SAtari911
20221d05cddcSAtari911        $html .= '</div>'; // End row
202319378907SAtari911
20241d05cddcSAtari911        // 4. IS REPEATING CHECKBOX
20251d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
20261d05cddcSAtari911        $html .= '<label class="checkbox-label checkbox-label-compact">';
202787ac9bf3SAtari911        $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">';
2028*da206178SAtari911        $html .= '<span>�� Repeating Event</span>';
202987ac9bf3SAtari911        $html .= '</label>';
203087ac9bf3SAtari911        $html .= '</div>';
203187ac9bf3SAtari911
20321d05cddcSAtari911        // Recurring options (shown when checkbox is checked)
203396df7d3eSAtari911        $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));">';
203487ac9bf3SAtari911
203596df7d3eSAtari911        // Row 1: Repeat every [N] [period]
203696df7d3eSAtari911        $html .= '<div class="form-row-group" style="margin-bottom:6px;">';
20371d05cddcSAtari911
203896df7d3eSAtari911        $html .= '<div class="form-field" style="flex:0 0 auto; min-width:0;">';
2039*da206178SAtari911        $html .= '<label class="field-label-compact">Repeat every</label>';
204096df7d3eSAtari911        $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;">';
204196df7d3eSAtari911        $html .= '</div>';
204296df7d3eSAtari911
204396df7d3eSAtari911        $html .= '<div class="form-field" style="flex:1; min-width:0;">';
204496df7d3eSAtari911        $html .= '<label class="field-label-compact">&nbsp;</label>';
204596df7d3eSAtari911        $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek input-compact" onchange="updateRecurrenceOptions(\'' . $calId . '\')">';
2046*da206178SAtari911        $html .= '<option value="daily">Day(s)</option>';
2047*da206178SAtari911        $html .= '<option value="weekly">Week(s)</option>';
2048*da206178SAtari911        $html .= '<option value="monthly">Month(s)</option>';
2049*da206178SAtari911        $html .= '<option value="yearly">Year(s)</option>';
205087ac9bf3SAtari911        $html .= '</select>';
205187ac9bf3SAtari911        $html .= '</div>';
205287ac9bf3SAtari911
205396df7d3eSAtari911        $html .= '</div>'; // End row 1
205496df7d3eSAtari911
205596df7d3eSAtari911        // Row 2: Weekly options - day of week checkboxes
205696df7d3eSAtari911        $html .= '<div id="weekly-options-' . $calId . '" class="weekly-options" style="display:none; margin-bottom:6px;">';
2057*da206178SAtari911        $html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">On these days:</label>';
205896df7d3eSAtari911        $html .= '<div style="display:flex; flex-wrap:wrap; gap:2px;">';
2059*da206178SAtari911        $dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
206096df7d3eSAtari911        foreach ($dayNames as $idx => $day) {
206196df7d3eSAtari911            $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;">';
206296df7d3eSAtari911            $html .= '<input type="checkbox" name="weekDays[]" value="' . $idx . '" style="margin-right:3px; width:12px; height:12px;">';
206396df7d3eSAtari911            $html .= '<span>' . $day . '</span>';
206496df7d3eSAtari911            $html .= '</label>';
206596df7d3eSAtari911        }
206696df7d3eSAtari911        $html .= '</div>';
206796df7d3eSAtari911        $html .= '</div>'; // End weekly options
206896df7d3eSAtari911
206996df7d3eSAtari911        // Row 3: Monthly options - day of month OR ordinal weekday
207096df7d3eSAtari911        $html .= '<div id="monthly-options-' . $calId . '" class="monthly-options" style="display:none; margin-bottom:6px;">';
2071*da206178SAtari911        $html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">Repeat on:</label>';
207296df7d3eSAtari911
207396df7d3eSAtari911        // Radio: Day of month vs Ordinal weekday
207496df7d3eSAtari911        $html .= '<div style="margin-bottom:6px;">';
207596df7d3eSAtari911        $html .= '<label style="display:inline-flex; align-items:center; margin-right:12px; cursor:pointer; font-size:11px;">';
207696df7d3eSAtari911        $html .= '<input type="radio" name="monthlyType" value="dayOfMonth" checked onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">';
2077*da206178SAtari911        $html .= 'Day of month';
207896df7d3eSAtari911        $html .= '</label>';
207996df7d3eSAtari911        $html .= '<label style="display:inline-flex; align-items:center; cursor:pointer; font-size:11px;">';
208096df7d3eSAtari911        $html .= '<input type="radio" name="monthlyType" value="ordinalWeekday" onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">';
2081*da206178SAtari911        $html .= 'Weekday pattern';
208296df7d3eSAtari911        $html .= '</label>';
208387ac9bf3SAtari911        $html .= '</div>';
208487ac9bf3SAtari911
208596df7d3eSAtari911        // Day of month input (shown by default)
208696df7d3eSAtari911        $html .= '<div id="monthly-day-' . $calId . '" style="display:flex; align-items:center; gap:6px;">';
2087*da206178SAtari911        $html .= '<span style="font-size:11px;">Day</span>';
208896df7d3eSAtari911        $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;">';
2089*da206178SAtari911        $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">of each month</span>';
209096df7d3eSAtari911        $html .= '</div>';
209196df7d3eSAtari911
209296df7d3eSAtari911        // Ordinal weekday (hidden by default)
209396df7d3eSAtari911        $html .= '<div id="monthly-ordinal-' . $calId . '" style="display:none;">';
209496df7d3eSAtari911        $html .= '<div style="display:flex; align-items:center; gap:4px; flex-wrap:wrap;">';
209596df7d3eSAtari911        $html .= '<select id="event-ordinal-' . $calId . '" name="ordinalWeek" class="input-sleek input-compact" style="width:auto;">';
2096*da206178SAtari911        $html .= '<option value="1">First</option>';
2097*da206178SAtari911        $html .= '<option value="2">Second</option>';
2098*da206178SAtari911        $html .= '<option value="3">Third</option>';
2099*da206178SAtari911        $html .= '<option value="4">Fourth</option>';
2100*da206178SAtari911        $html .= '<option value="5">Fifth</option>';
2101*da206178SAtari911        $html .= '<option value="-1">Last</option>';
210296df7d3eSAtari911        $html .= '</select>';
210396df7d3eSAtari911        $html .= '<select id="event-ordinal-day-' . $calId . '" name="ordinalDay" class="input-sleek input-compact" style="width:auto;">';
2104*da206178SAtari911        $html .= '<option value="0">Sunday</option>';
2105*da206178SAtari911        $html .= '<option value="1">Monday</option>';
2106*da206178SAtari911        $html .= '<option value="2">Tuesday</option>';
2107*da206178SAtari911        $html .= '<option value="3">Wednesday</option>';
2108*da206178SAtari911        $html .= '<option value="4">Thursday</option>';
2109*da206178SAtari911        $html .= '<option value="5">Friday</option>';
2110*da206178SAtari911        $html .= '<option value="6">Saturday</option>';
211196df7d3eSAtari911        $html .= '</select>';
2112*da206178SAtari911        $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">of each month</span>';
211396df7d3eSAtari911        $html .= '</div>';
211496df7d3eSAtari911        $html .= '</div>';
211596df7d3eSAtari911
211696df7d3eSAtari911        $html .= '</div>'; // End monthly options
211796df7d3eSAtari911
211896df7d3eSAtari911        // Row 4: End date
211996df7d3eSAtari911        $html .= '<div class="form-row-group">';
212096df7d3eSAtari911        $html .= '<div class="form-field">';
2121*da206178SAtari911        $html .= '<label class="field-label-compact">Repeat Until (optional)</label>';
212296df7d3eSAtari911        $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date input-compact" placeholder="Optional">';
2123*da206178SAtari911        $html .= '<div style="font-size:9px; color:var(--text-dim, #666); margin-top:2px;">Leave empty for 1 year of events</div>';
212496df7d3eSAtari911        $html .= '</div>';
212596df7d3eSAtari911        $html .= '</div>'; // End row 4
212696df7d3eSAtari911
21271d05cddcSAtari911        $html .= '</div>'; // End recurring options
212887ac9bf3SAtari911
21291d05cddcSAtari911        // 5. TIME (Start & End) - COLOR (inline)
21301d05cddcSAtari911        $html .= '<div class="form-row-group">';
21311d05cddcSAtari911
21321d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
2133*da206178SAtari911        $html .= '<label class="field-label-compact">�� Start Time</label>';
2134*da206178SAtari911        $html .= '<div class="time-picker-wrapper">';
2135*da206178SAtari911        $html .= '<select id="event-time-' . $calId . '" name="time" class="input-sleek input-compact time-select" onchange="updateEndTimeOptions(\'' . $calId . '\')">';
2136*da206178SAtari911        $html .= '<option value="">All day</option>';
2137e3a9f44cSAtari911
2138*da206178SAtari911        // Generate time options grouped by period
2139*da206178SAtari911        $periods = [
2140*da206178SAtari911            'Morning' => [6, 7, 8, 9, 10, 11],
2141*da206178SAtari911            'Afternoon' => [12, 13, 14, 15, 16, 17],
2142*da206178SAtari911            'Evening' => [18, 19, 20, 21, 22, 23],
2143*da206178SAtari911            'Night' => [0, 1, 2, 3, 4, 5]
2144*da206178SAtari911        ];
2145*da206178SAtari911
2146*da206178SAtari911        foreach ($periods as $periodName => $hours) {
2147*da206178SAtari911            $html .= '<optgroup label="── ' . $periodName . ' ──">';
2148*da206178SAtari911            foreach ($hours as $hour) {
2149e3a9f44cSAtari911                for ($minute = 0; $minute < 60; $minute += 15) {
2150e3a9f44cSAtari911                    $timeValue = sprintf('%02d:%02d', $hour, $minute);
2151e3a9f44cSAtari911                    $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
2152e3a9f44cSAtari911                    $ampm = $hour < 12 ? 'AM' : 'PM';
2153e3a9f44cSAtari911                    $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
2154e3a9f44cSAtari911                    $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
2155e3a9f44cSAtari911                }
2156e3a9f44cSAtari911            }
2157*da206178SAtari911            $html .= '</optgroup>';
2158*da206178SAtari911        }
2159e3a9f44cSAtari911
2160e3a9f44cSAtari911        $html .= '</select>';
216119378907SAtari911        $html .= '</div>';
2162*da206178SAtari911        $html .= '</div>';
216319378907SAtari911
21641d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
2165*da206178SAtari911        $html .= '<label class="field-label-compact">�� End Time</label>';
2166*da206178SAtari911        $html .= '<div class="time-picker-wrapper">';
2167*da206178SAtari911        $html .= '<select id="event-end-time-' . $calId . '" name="endTime" class="input-sleek input-compact time-select">';
2168*da206178SAtari911        $html .= '<option value="">Same as start</option>';
21691d05cddcSAtari911
2170*da206178SAtari911        // Generate time options grouped by period (same as start time)
2171*da206178SAtari911        foreach ($periods as $periodName => $hours) {
2172*da206178SAtari911            $html .= '<optgroup label="── ' . $periodName . ' ──">';
2173*da206178SAtari911            foreach ($hours as $hour) {
21741d05cddcSAtari911                for ($minute = 0; $minute < 60; $minute += 15) {
21751d05cddcSAtari911                    $timeValue = sprintf('%02d:%02d', $hour, $minute);
21761d05cddcSAtari911                    $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
21771d05cddcSAtari911                    $ampm = $hour < 12 ? 'AM' : 'PM';
21781d05cddcSAtari911                    $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
21791d05cddcSAtari911                    $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
21801d05cddcSAtari911                }
21811d05cddcSAtari911            }
2182*da206178SAtari911            $html .= '</optgroup>';
2183*da206178SAtari911        }
21841d05cddcSAtari911
21851d05cddcSAtari911        $html .= '</select>';
218619378907SAtari911        $html .= '</div>';
2187*da206178SAtari911        $html .= '</div>';
218819378907SAtari911
21891d05cddcSAtari911        $html .= '</div>'; // End row
21901d05cddcSAtari911
21911d05cddcSAtari911        // Color field (new row)
21921d05cddcSAtari911        $html .= '<div class="form-row-group">';
21931d05cddcSAtari911
21941d05cddcSAtari911        $html .= '<div class="form-field form-field-full">';
2195*da206178SAtari911        $html .= '<label class="field-label-compact">�� Color</label>';
21961d05cddcSAtari911        $html .= '<div class="color-picker-wrapper">';
21971d05cddcSAtari911        $html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact color-select" onchange="updateCustomColorPicker(\'' . $calId . '\')">';
2198*da206178SAtari911        $html .= '<option value="#3498db" style="background:#3498db;color:white">�� Blue</option>';
2199*da206178SAtari911        $html .= '<option value="#2ecc71" style="background:#2ecc71;color:white">�� Green</option>';
2200*da206178SAtari911        $html .= '<option value="#e74c3c" style="background:#e74c3c;color:white">�� Red</option>';
2201*da206178SAtari911        $html .= '<option value="#f39c12" style="background:#f39c12;color:white">�� Orange</option>';
2202*da206178SAtari911        $html .= '<option value="#9b59b6" style="background:#9b59b6;color:white">�� Purple</option>';
2203*da206178SAtari911        $html .= '<option value="#e91e63" style="background:#e91e63;color:white">�� Pink</option>';
2204*da206178SAtari911        $html .= '<option value="#1abc9c" style="background:#1abc9c;color:white">�� Teal</option>';
2205*da206178SAtari911        $html .= '<option value="custom">�� Custom...</option>';
22061d05cddcSAtari911        $html .= '</select>';
22071d05cddcSAtari911        $html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input color-picker-compact" value="#3498db" onchange="updateColorFromPicker(\'' . $calId . '\')">';
22081d05cddcSAtari911        $html .= '</div>';
220919378907SAtari911        $html .= '</div>';
221019378907SAtari911
22111d05cddcSAtari911        $html .= '</div>'; // End row
22121d05cddcSAtari911
22131d05cddcSAtari911        // Task checkbox
22141d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
22151d05cddcSAtari911        $html .= '<label class="checkbox-label checkbox-label-compact">';
22161d05cddcSAtari911        $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">';
2217*da206178SAtari911        $html .= '<span>�� This is a task (can be checked off)</span>';
22181d05cddcSAtari911        $html .= '</label>';
221919378907SAtari911        $html .= '</div>';
222019378907SAtari911
222119378907SAtari911        // Action buttons
222219378907SAtari911        $html .= '<div class="dialog-actions-sleek">';
2223*da206178SAtari911        $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>';
2224*da206178SAtari911        $html .= '<button type="submit" class="btn-sleek btn-save-sleek">�� Save</button>';
222519378907SAtari911        $html .= '</div>';
222619378907SAtari911
222719378907SAtari911        $html .= '</form>';
222819378907SAtari911        $html .= '</div>';
222919378907SAtari911        $html .= '</div>';
223019378907SAtari911
223119378907SAtari911        return $html;
223219378907SAtari911    }
223319378907SAtari911
22349ccd446eSAtari911    private function renderMonthPicker($calId, $year, $month, $namespace, $theme = 'matrix', $themeStyles = null) {
22359ccd446eSAtari911        // Fallback to default theme if not provided
22369ccd446eSAtari911        if ($themeStyles === null) {
22379ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
22389ccd446eSAtari911        }
22399ccd446eSAtari911
22409ccd446eSAtari911        $themeClass = 'calendar-theme-' . $theme;
22419ccd446eSAtari911
22429ccd446eSAtari911        $html = '<div class="month-picker-overlay ' . $themeClass . '" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">';
224387ac9bf3SAtari911        $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">';
224487ac9bf3SAtari911        $html .= '<h4>Jump to Month</h4>';
224587ac9bf3SAtari911
224687ac9bf3SAtari911        $html .= '<div class="month-picker-selects">';
224787ac9bf3SAtari911        $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">';
224887ac9bf3SAtari911        $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
224987ac9bf3SAtari911        for ($m = 1; $m <= 12; $m++) {
225087ac9bf3SAtari911            $selected = ($m == $month) ? ' selected' : '';
225187ac9bf3SAtari911            $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>';
225287ac9bf3SAtari911        }
225387ac9bf3SAtari911        $html .= '</select>';
225487ac9bf3SAtari911
225587ac9bf3SAtari911        $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">';
225687ac9bf3SAtari911        $currentYear = (int)date('Y');
225787ac9bf3SAtari911        for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) {
225887ac9bf3SAtari911            $selected = ($y == $year) ? ' selected' : '';
225987ac9bf3SAtari911            $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>';
226087ac9bf3SAtari911        }
226187ac9bf3SAtari911        $html .= '</select>';
226287ac9bf3SAtari911        $html .= '</div>';
226387ac9bf3SAtari911
226487ac9bf3SAtari911        $html .= '<div class="month-picker-actions">';
226587ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>';
226687ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>';
226787ac9bf3SAtari911        $html .= '</div>';
226887ac9bf3SAtari911
226987ac9bf3SAtari911        $html .= '</div>';
227087ac9bf3SAtari911        $html .= '</div>';
227187ac9bf3SAtari911
227287ac9bf3SAtari911        return $html;
227387ac9bf3SAtari911    }
227487ac9bf3SAtari911
22759ccd446eSAtari911    private function renderDescription($description, $themeStyles = null) {
227619378907SAtari911        if (empty($description)) {
227719378907SAtari911            return '';
227819378907SAtari911        }
227919378907SAtari911
22809ccd446eSAtari911        // Get theme for link colors if not provided
22819ccd446eSAtari911        if ($themeStyles === null) {
22829ccd446eSAtari911            $theme = $this->getSidebarTheme();
22839ccd446eSAtari911            $themeStyles = $this->getSidebarThemeStyles($theme);
22849ccd446eSAtari911        }
22859ccd446eSAtari911
22869ccd446eSAtari911        $linkColor = '';
22879ccd446eSAtari911        $linkStyle = ' class="cal-link"';
22889ccd446eSAtari911
2289e3a9f44cSAtari911        // Token-based parsing to avoid escaping issues
2290e3a9f44cSAtari911        $rendered = $description;
2291e3a9f44cSAtari911        $tokens = array();
2292e3a9f44cSAtari911        $tokenIndex = 0;
229319378907SAtari911
2294e3a9f44cSAtari911        // Convert DokuWiki image syntax {{image.jpg}} to tokens
2295e3a9f44cSAtari911        $pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/';
2296e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2297e3a9f44cSAtari911        foreach ($matches as $match) {
2298e3a9f44cSAtari911            $imagePath = trim($match[1]);
2299e3a9f44cSAtari911            $alt = isset($match[2]) ? trim($match[2]) : '';
230019378907SAtari911
2301e3a9f44cSAtari911            // Handle external URLs
230219378907SAtari911            if (preg_match('/^https?:\/\//', $imagePath)) {
2303e3a9f44cSAtari911                $imageHtml = '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
2304e3a9f44cSAtari911            } else {
230519378907SAtari911                // Handle internal DokuWiki images
230619378907SAtari911                $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath);
2307e3a9f44cSAtari911                $imageHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
2308e3a9f44cSAtari911            }
230919378907SAtari911
2310e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2311e3a9f44cSAtari911            $tokens[$tokenIndex] = $imageHtml;
2312e3a9f44cSAtari911            $tokenIndex++;
2313e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
2314e3a9f44cSAtari911        }
2315e3a9f44cSAtari911
2316e3a9f44cSAtari911        // Convert DokuWiki link syntax [[link|text]] to tokens
2317e3a9f44cSAtari911        $pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/';
2318e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2319e3a9f44cSAtari911        foreach ($matches as $match) {
2320e3a9f44cSAtari911            $link = trim($match[1]);
2321e3a9f44cSAtari911            $text = isset($match[2]) ? trim($match[2]) : $link;
232219378907SAtari911
232319378907SAtari911            // Handle external URLs
232419378907SAtari911            if (preg_match('/^https?:\/\//', $link)) {
23259ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
2326e3a9f44cSAtari911            } else {
232787ac9bf3SAtari911                // Handle internal DokuWiki links with section anchors
232887ac9bf3SAtari911                $parts = explode('#', $link, 2);
232987ac9bf3SAtari911                $pagePart = $parts[0];
233087ac9bf3SAtari911                $sectionPart = isset($parts[1]) ? '#' . $parts[1] : '';
233187ac9bf3SAtari911
233287ac9bf3SAtari911                $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart;
23339ccd446eSAtari911                $linkHtml = '<a href="' . $wikiUrl . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
233419378907SAtari911            }
233519378907SAtari911
2336e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2337e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
2338e3a9f44cSAtari911            $tokenIndex++;
2339e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
2340e3a9f44cSAtari911        }
234119378907SAtari911
2342e3a9f44cSAtari911        // Convert markdown-style links [text](url) to tokens
2343e3a9f44cSAtari911        $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
2344e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2345e3a9f44cSAtari911        foreach ($matches as $match) {
2346e3a9f44cSAtari911            $text = trim($match[1]);
2347e3a9f44cSAtari911            $url = trim($match[2]);
234819378907SAtari911
2349e3a9f44cSAtari911            if (preg_match('/^https?:\/\//', $url)) {
23509ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
2351e3a9f44cSAtari911            } else {
23529ccd446eSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
2353e3a9f44cSAtari911            }
2354e3a9f44cSAtari911
2355e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2356e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
2357e3a9f44cSAtari911            $tokenIndex++;
2358e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
2359e3a9f44cSAtari911        }
2360e3a9f44cSAtari911
2361e3a9f44cSAtari911        // Convert plain URLs to tokens
2362e3a9f44cSAtari911        $pattern = '/(https?:\/\/[^\s<]+)/';
2363e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2364e3a9f44cSAtari911        foreach ($matches as $match) {
2365e3a9f44cSAtari911            $url = $match[1];
23669ccd446eSAtari911            $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($url) . '</a>';
2367e3a9f44cSAtari911
2368e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2369e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
2370e3a9f44cSAtari911            $tokenIndex++;
2371e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
2372e3a9f44cSAtari911        }
2373e3a9f44cSAtari911
2374e3a9f44cSAtari911        // NOW escape HTML (tokens are protected)
2375e3a9f44cSAtari911        $rendered = htmlspecialchars($rendered);
2376e3a9f44cSAtari911
2377e3a9f44cSAtari911        // Convert newlines to <br>
2378e3a9f44cSAtari911        $rendered = nl2br($rendered);
2379e3a9f44cSAtari911
2380e3a9f44cSAtari911        // DokuWiki text formatting
2381e3a9f44cSAtari911        // Bold: **text** or __text__
23829ccd446eSAtari911        $boldStyle = '';
2383e3a9f44cSAtari911        $rendered = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $rendered);
2384e3a9f44cSAtari911        $rendered = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $rendered);
2385e3a9f44cSAtari911
2386e3a9f44cSAtari911        // Italic: //text//
2387e3a9f44cSAtari911        $rendered = preg_replace('/\/\/(.+?)\/\//', '<em>$1</em>', $rendered);
2388e3a9f44cSAtari911
2389e3a9f44cSAtari911        // Strikethrough: <del>text</del>
2390e3a9f44cSAtari911        $rendered = preg_replace('/&lt;del&gt;(.+?)&lt;\/del&gt;/', '<del>$1</del>', $rendered);
2391e3a9f44cSAtari911
2392e3a9f44cSAtari911        // Monospace: ''text''
2393e3a9f44cSAtari911        $rendered = preg_replace('/&#039;&#039;(.+?)&#039;&#039;/', '<code>$1</code>', $rendered);
2394e3a9f44cSAtari911
2395e3a9f44cSAtari911        // Subscript: <sub>text</sub>
2396e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sub&gt;(.+?)&lt;\/sub&gt;/', '<sub>$1</sub>', $rendered);
2397e3a9f44cSAtari911
2398e3a9f44cSAtari911        // Superscript: <sup>text</sup>
2399e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sup&gt;(.+?)&lt;\/sup&gt;/', '<sup>$1</sup>', $rendered);
2400e3a9f44cSAtari911
2401e3a9f44cSAtari911        // Restore tokens
2402e3a9f44cSAtari911        foreach ($tokens as $i => $html) {
2403e3a9f44cSAtari911            $rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered);
2404e3a9f44cSAtari911        }
240519378907SAtari911
240619378907SAtari911        return $rendered;
240719378907SAtari911    }
240819378907SAtari911
240919378907SAtari911    private function loadEvents($namespace, $year, $month) {
241019378907SAtari911        $dataDir = DOKU_INC . 'data/meta/';
241119378907SAtari911        if ($namespace) {
241219378907SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
241319378907SAtari911        }
241419378907SAtari911        $dataDir .= 'calendar/';
241519378907SAtari911
241619378907SAtari911        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
241719378907SAtari911
241819378907SAtari911        if (file_exists($eventFile)) {
241919378907SAtari911            $json = file_get_contents($eventFile);
242019378907SAtari911            return json_decode($json, true);
242119378907SAtari911        }
242219378907SAtari911
242319378907SAtari911        return array();
242419378907SAtari911    }
2425e3a9f44cSAtari911
2426e3a9f44cSAtari911    private function loadEventsMultiNamespace($namespaces, $year, $month) {
2427e3a9f44cSAtari911        // Check for wildcard pattern (namespace:*)
2428e3a9f44cSAtari911        if (preg_match('/^(.+):\*$/', $namespaces, $matches)) {
2429e3a9f44cSAtari911            $baseNamespace = $matches[1];
2430e3a9f44cSAtari911            return $this->loadEventsWildcard($baseNamespace, $year, $month);
2431e3a9f44cSAtari911        }
2432e3a9f44cSAtari911
2433e3a9f44cSAtari911        // Check for root wildcard (just *)
2434e3a9f44cSAtari911        if ($namespaces === '*') {
2435e3a9f44cSAtari911            return $this->loadEventsWildcard('', $year, $month);
2436e3a9f44cSAtari911        }
2437e3a9f44cSAtari911
2438e3a9f44cSAtari911        // Parse namespace list (semicolon separated)
2439e3a9f44cSAtari911        // e.g., "team:projects;personal;work:tasks" = three namespaces
2440e3a9f44cSAtari911        $namespaceList = array_map('trim', explode(';', $namespaces));
2441e3a9f44cSAtari911
2442e3a9f44cSAtari911        // Load events from all namespaces
2443e3a9f44cSAtari911        $allEvents = array();
2444e3a9f44cSAtari911        foreach ($namespaceList as $ns) {
2445e3a9f44cSAtari911            $ns = trim($ns);
2446e3a9f44cSAtari911            if (empty($ns)) continue;
2447e3a9f44cSAtari911
2448e3a9f44cSAtari911            $events = $this->loadEvents($ns, $year, $month);
2449e3a9f44cSAtari911
2450e3a9f44cSAtari911            // Add namespace tag to each event
2451e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
2452e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
2453e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
2454e3a9f44cSAtari911                }
2455e3a9f44cSAtari911                foreach ($dayEvents as $event) {
2456e3a9f44cSAtari911                    $event['_namespace'] = $ns;
2457e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
2458e3a9f44cSAtari911                }
2459e3a9f44cSAtari911            }
2460e3a9f44cSAtari911        }
2461e3a9f44cSAtari911
2462e3a9f44cSAtari911        return $allEvents;
2463e3a9f44cSAtari911    }
2464e3a9f44cSAtari911
2465e3a9f44cSAtari911    private function loadEventsWildcard($baseNamespace, $year, $month) {
2466e3a9f44cSAtari911        // Find all subdirectories under the base namespace
2467e3a9f44cSAtari911        $dataDir = DOKU_INC . 'data/meta/';
2468e3a9f44cSAtari911        if ($baseNamespace) {
2469e3a9f44cSAtari911            $dataDir .= str_replace(':', '/', $baseNamespace) . '/';
2470e3a9f44cSAtari911        }
2471e3a9f44cSAtari911
2472e3a9f44cSAtari911        $allEvents = array();
2473e3a9f44cSAtari911
2474e3a9f44cSAtari911        // First, load events from the base namespace itself
2475e3a9f44cSAtari911        if (empty($baseNamespace)) {
2476e3a9f44cSAtari911            // Root wildcard - load from root calendar
2477e3a9f44cSAtari911            $events = $this->loadEvents('', $year, $month);
2478e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
2479e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
2480e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
2481e3a9f44cSAtari911                }
2482e3a9f44cSAtari911                foreach ($dayEvents as $event) {
2483e3a9f44cSAtari911                    $event['_namespace'] = '';
2484e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
2485e3a9f44cSAtari911                }
2486e3a9f44cSAtari911            }
2487e3a9f44cSAtari911        } else {
2488e3a9f44cSAtari911            $events = $this->loadEvents($baseNamespace, $year, $month);
2489e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
2490e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
2491e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
2492e3a9f44cSAtari911                }
2493e3a9f44cSAtari911                foreach ($dayEvents as $event) {
2494e3a9f44cSAtari911                    $event['_namespace'] = $baseNamespace;
2495e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
2496e3a9f44cSAtari911                }
2497e3a9f44cSAtari911            }
2498e3a9f44cSAtari911        }
2499e3a9f44cSAtari911
2500e3a9f44cSAtari911        // Recursively find all subdirectories
2501e3a9f44cSAtari911        $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents);
2502e3a9f44cSAtari911
2503e3a9f44cSAtari911        return $allEvents;
2504e3a9f44cSAtari911    }
2505e3a9f44cSAtari911
2506e3a9f44cSAtari911    private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) {
2507e3a9f44cSAtari911        if (!is_dir($dir)) return;
2508e3a9f44cSAtari911
2509e3a9f44cSAtari911        $items = scandir($dir);
2510e3a9f44cSAtari911        foreach ($items as $item) {
2511e3a9f44cSAtari911            if ($item === '.' || $item === '..') continue;
2512e3a9f44cSAtari911
2513e3a9f44cSAtari911            $path = $dir . $item;
2514e3a9f44cSAtari911            if (is_dir($path) && $item !== 'calendar') {
2515e3a9f44cSAtari911                // This is a namespace directory
2516e3a9f44cSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
2517e3a9f44cSAtari911
2518e3a9f44cSAtari911                // Load events from this namespace
2519e3a9f44cSAtari911                $events = $this->loadEvents($namespace, $year, $month);
2520e3a9f44cSAtari911                foreach ($events as $dateKey => $dayEvents) {
2521e3a9f44cSAtari911                    if (!isset($allEvents[$dateKey])) {
2522e3a9f44cSAtari911                        $allEvents[$dateKey] = array();
2523e3a9f44cSAtari911                    }
2524e3a9f44cSAtari911                    foreach ($dayEvents as $event) {
2525e3a9f44cSAtari911                        $event['_namespace'] = $namespace;
2526e3a9f44cSAtari911                        $allEvents[$dateKey][] = $event;
2527e3a9f44cSAtari911                    }
2528e3a9f44cSAtari911                }
2529e3a9f44cSAtari911
2530e3a9f44cSAtari911                // Recurse into subdirectories
2531e3a9f44cSAtari911                $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents);
2532e3a9f44cSAtari911            }
2533e3a9f44cSAtari911        }
2534e3a9f44cSAtari911    }
25351d05cddcSAtari911
25361d05cddcSAtari911    private function getAllNamespaces() {
25371d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
25381d05cddcSAtari911        $namespaces = [];
25391d05cddcSAtari911
25401d05cddcSAtari911        // Scan for namespaces that have calendar data
25411d05cddcSAtari911        $this->scanForCalendarNamespaces($dataDir, '', $namespaces);
25421d05cddcSAtari911
25431d05cddcSAtari911        // Sort alphabetically
25441d05cddcSAtari911        sort($namespaces);
25451d05cddcSAtari911
25461d05cddcSAtari911        return $namespaces;
25471d05cddcSAtari911    }
25481d05cddcSAtari911
25491d05cddcSAtari911    private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) {
25501d05cddcSAtari911        if (!is_dir($dir)) return;
25511d05cddcSAtari911
25521d05cddcSAtari911        $items = scandir($dir);
25531d05cddcSAtari911        foreach ($items as $item) {
25541d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
25551d05cddcSAtari911
25561d05cddcSAtari911            $path = $dir . $item;
25571d05cddcSAtari911            if (is_dir($path)) {
25581d05cddcSAtari911                // Check if this directory has a calendar subdirectory with data
25591d05cddcSAtari911                $calendarDir = $path . '/calendar/';
25601d05cddcSAtari911                if (is_dir($calendarDir)) {
25611d05cddcSAtari911                    // Check if there are any JSON files in the calendar directory
25621d05cddcSAtari911                    $jsonFiles = glob($calendarDir . '*.json');
25631d05cddcSAtari911                    if (!empty($jsonFiles)) {
25641d05cddcSAtari911                        // This namespace has calendar data
25651d05cddcSAtari911                        $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
25661d05cddcSAtari911                        $namespaces[] = $namespace;
25671d05cddcSAtari911                    }
25681d05cddcSAtari911                }
25691d05cddcSAtari911
25701d05cddcSAtari911                // Recurse into subdirectories
25711d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
25721d05cddcSAtari911                $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces);
25731d05cddcSAtari911            }
25741d05cddcSAtari911        }
25751d05cddcSAtari911    }
25761d05cddcSAtari911
25771d05cddcSAtari911    /**
25781d05cddcSAtari911     * Render new sidebar widget - Week at a glance itinerary (200px wide)
25791d05cddcSAtari911     */
25800c3b6e81SAtari911    private function renderSidebarWidget($events, $namespace, $calId, $themeOverride = null) {
25811d05cddcSAtari911        if (empty($events)) {
2582*da206178SAtari911            return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">No events this week</div>';
25831d05cddcSAtari911        }
25841d05cddcSAtari911
25851d05cddcSAtari911        // Get important namespaces from config
25861d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
25871d05cddcSAtari911        $importantNsList = ['important']; // default
25881d05cddcSAtari911        if (file_exists($configFile)) {
25891d05cddcSAtari911            $config = include $configFile;
25901d05cddcSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
25911d05cddcSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
25921d05cddcSAtari911            }
25931d05cddcSAtari911        }
25941d05cddcSAtari911
25951d05cddcSAtari911        // Calculate date ranges
25961d05cddcSAtari911        $todayStr = date('Y-m-d');
25971d05cddcSAtari911        $tomorrowStr = date('Y-m-d', strtotime('+1 day'));
25989ccd446eSAtari911
25999ccd446eSAtari911        // Get week start preference and calculate week range
26009ccd446eSAtari911        $weekStartDay = $this->getWeekStartDay();
26019ccd446eSAtari911
26029ccd446eSAtari911        if ($weekStartDay === 'monday') {
26039ccd446eSAtari911            // Monday start
26041d05cddcSAtari911            $weekStart = date('Y-m-d', strtotime('monday this week'));
26051d05cddcSAtari911            $weekEnd = date('Y-m-d', strtotime('sunday this week'));
26069ccd446eSAtari911        } else {
26079ccd446eSAtari911            // Sunday start (default - US/Canada standard)
26089ccd446eSAtari911            $today = date('w'); // 0 (Sun) to 6 (Sat)
26099ccd446eSAtari911            if ($today == 0) {
26109ccd446eSAtari911                // Today is Sunday
26119ccd446eSAtari911                $weekStart = date('Y-m-d');
26129ccd446eSAtari911            } else {
26139ccd446eSAtari911                // Monday-Saturday: go back to last Sunday
26149ccd446eSAtari911                $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
26159ccd446eSAtari911            }
26169ccd446eSAtari911            $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
26179ccd446eSAtari911        }
26181d05cddcSAtari911
26191d05cddcSAtari911        // Group events by category
26201d05cddcSAtari911        $todayEvents = [];
26211d05cddcSAtari911        $tomorrowEvents = [];
26221d05cddcSAtari911        $importantEvents = [];
26231d05cddcSAtari911        $weekEvents = []; // For week grid
26241d05cddcSAtari911
26251d05cddcSAtari911        // Process all events
26261d05cddcSAtari911        foreach ($events as $dateKey => $dayEvents) {
26279ccd446eSAtari911            // Detect conflicts for events on this day
26289ccd446eSAtari911            $eventsWithConflicts = $this->detectTimeConflicts($dayEvents);
26291d05cddcSAtari911
26309ccd446eSAtari911            foreach ($eventsWithConflicts as $event) {
26319ccd446eSAtari911                // Always categorize Today and Tomorrow regardless of week boundaries
26329ccd446eSAtari911                if ($dateKey === $todayStr) {
26339ccd446eSAtari911                    $todayEvents[] = array_merge($event, ['date' => $dateKey]);
26349ccd446eSAtari911                }
26359ccd446eSAtari911                if ($dateKey === $tomorrowStr) {
26369ccd446eSAtari911                    $tomorrowEvents[] = array_merge($event, ['date' => $dateKey]);
26379ccd446eSAtari911                }
26389ccd446eSAtari911
26399ccd446eSAtari911                // Process week grid events (only for current week)
26401d05cddcSAtari911                if ($dateKey >= $weekStart && $dateKey <= $weekEnd) {
26419ccd446eSAtari911                    // Initialize week grid day if not exists
26421d05cddcSAtari911                    if (!isset($weekEvents[$dateKey])) {
26431d05cddcSAtari911                        $weekEvents[$dateKey] = [];
26441d05cddcSAtari911                    }
26451d05cddcSAtari911
26461d05cddcSAtari911                    // Pre-render DokuWiki syntax to HTML for JavaScript display
26471d05cddcSAtari911                    $eventWithHtml = $event;
26481d05cddcSAtari911                    if (isset($event['title'])) {
26491d05cddcSAtari911                        $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']);
26501d05cddcSAtari911                    }
26511d05cddcSAtari911                    if (isset($event['description'])) {
26521d05cddcSAtari911                        $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']);
26531d05cddcSAtari911                    }
26541d05cddcSAtari911                    $weekEvents[$dateKey][] = $eventWithHtml;
26551d05cddcSAtari911                }
26561d05cddcSAtari911
26571d05cddcSAtari911                // Check if this is an important namespace
26581d05cddcSAtari911                $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
26591d05cddcSAtari911                $isImportant = false;
26601d05cddcSAtari911                foreach ($importantNsList as $impNs) {
26611d05cddcSAtari911                    if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
26621d05cddcSAtari911                        $isImportant = true;
26631d05cddcSAtari911                        break;
26641d05cddcSAtari911                    }
26651d05cddcSAtari911                }
26661d05cddcSAtari911
26679ccd446eSAtari911                // Important events: show from today through next 2 weeks
26689ccd446eSAtari911                if ($isImportant && $dateKey >= $todayStr) {
26691d05cddcSAtari911                    $importantEvents[] = array_merge($event, ['date' => $dateKey]);
26701d05cddcSAtari911                }
26711d05cddcSAtari911            }
26721d05cddcSAtari911        }
26739ccd446eSAtari911
26749ccd446eSAtari911        // Sort Important Events by date (earliest first)
26759ccd446eSAtari911        usort($importantEvents, function($a, $b) {
26769ccd446eSAtari911            $dateA = isset($a['date']) ? $a['date'] : '';
26779ccd446eSAtari911            $dateB = isset($b['date']) ? $b['date'] : '';
26789ccd446eSAtari911
26799ccd446eSAtari911            // Compare dates
26809ccd446eSAtari911            if ($dateA === $dateB) {
26819ccd446eSAtari911                // Same date - sort by time
26829ccd446eSAtari911                $timeA = isset($a['time']) ? $a['time'] : '';
26839ccd446eSAtari911                $timeB = isset($b['time']) ? $b['time'] : '';
26849ccd446eSAtari911
26859ccd446eSAtari911                if (empty($timeA) && !empty($timeB)) return 1;  // All-day events last
26869ccd446eSAtari911                if (!empty($timeA) && empty($timeB)) return -1;
26879ccd446eSAtari911                if (empty($timeA) && empty($timeB)) return 0;
26889ccd446eSAtari911
26899ccd446eSAtari911                // Both have times
26909ccd446eSAtari911                $aMinutes = $this->timeToMinutes($timeA);
26919ccd446eSAtari911                $bMinutes = $this->timeToMinutes($timeB);
26929ccd446eSAtari911                return $aMinutes - $bMinutes;
26931d05cddcSAtari911            }
26941d05cddcSAtari911
26959ccd446eSAtari911            return strcmp($dateA, $dateB);
26969ccd446eSAtari911        });
26979ccd446eSAtari911
26980c3b6e81SAtari911        // Get theme - prefer override from syntax parameter, fall back to admin default
26990c3b6e81SAtari911        $theme = !empty($themeOverride) ? $themeOverride : $this->getSidebarTheme();
27009ccd446eSAtari911        $themeStyles = $this->getSidebarThemeStyles($theme);
27019ccd446eSAtari911        $themeClass = 'sidebar-' . $theme;
27029ccd446eSAtari911
27039ccd446eSAtari911        // Start building HTML - Dynamic width with default font (overflow:visible for tooltips)
27049ccd446eSAtari911        $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;">';
27059ccd446eSAtari911
27069ccd446eSAtari911        // Inject CSS variables so the event dialog (shared component) picks up the theme
27079ccd446eSAtari911        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
27089ccd446eSAtari911        $html .= '<style>
27099ccd446eSAtari911        #sidebar-widget-' . $calId . ' {
27109ccd446eSAtari911            --background-site: ' . $themeStyles['bg'] . ';
27119ccd446eSAtari911            --background-alt: ' . $themeStyles['cell_bg'] . ';
27129ccd446eSAtari911            --background-header: ' . $themeStyles['header_bg'] . ';
27139ccd446eSAtari911            --text-primary: ' . $themeStyles['text_primary'] . ';
27149ccd446eSAtari911            --text-dim: ' . $themeStyles['text_dim'] . ';
27159ccd446eSAtari911            --text-bright: ' . $themeStyles['text_bright'] . ';
27169ccd446eSAtari911            --border-color: ' . $themeStyles['grid_border'] . ';
27179ccd446eSAtari911            --border-main: ' . $themeStyles['border'] . ';
27189ccd446eSAtari911            --cell-bg: ' . $themeStyles['cell_bg'] . ';
27199ccd446eSAtari911            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
27209ccd446eSAtari911            --shadow-color: ' . $themeStyles['shadow'] . ';
27219ccd446eSAtari911            --header-border: ' . $themeStyles['header_border'] . ';
27229ccd446eSAtari911            --header-shadow: ' . $themeStyles['header_shadow'] . ';
27239ccd446eSAtari911            --grid-bg: ' . $themeStyles['grid_bg'] . ';
27249ccd446eSAtari911            --btn-text: ' . $btnTextColor . ';
27257e8ea635SAtari911            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
27267e8ea635SAtari911            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
27277e8ea635SAtari911            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
27287e8ea635SAtari911            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
27297e8ea635SAtari911            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
27307e8ea635SAtari911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
27317e8ea635SAtari911            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
27329ccd446eSAtari911        }
27339ccd446eSAtari911        </style>';
27349ccd446eSAtari911
27359ccd446eSAtari911        // Add sparkle effect for pink theme
27369ccd446eSAtari911        if ($theme === 'pink') {
27379ccd446eSAtari911            $html .= '<style>
27389ccd446eSAtari911            @keyframes sparkle-' . $calId . ' {
27399ccd446eSAtari911                0% {
27409ccd446eSAtari911                    opacity: 0;
27419ccd446eSAtari911                    transform: translate(0, 0) scale(0) rotate(0deg);
27429ccd446eSAtari911                }
27439ccd446eSAtari911                50% {
27449ccd446eSAtari911                    opacity: 1;
27459ccd446eSAtari911                    transform: translate(var(--tx), var(--ty)) scale(1) rotate(180deg);
27469ccd446eSAtari911                }
27479ccd446eSAtari911                100% {
27489ccd446eSAtari911                    opacity: 0;
27499ccd446eSAtari911                    transform: translate(calc(var(--tx) * 2), calc(var(--ty) * 2)) scale(0) rotate(360deg);
27509ccd446eSAtari911                }
27519ccd446eSAtari911            }
27529ccd446eSAtari911
27539ccd446eSAtari911            @keyframes pulse-glow-' . $calId . ' {
27549ccd446eSAtari911                0%, 100% { box-shadow: 0 0 10px rgba(255, 20, 147, 0.4); }
27559ccd446eSAtari911                50% { box-shadow: 0 0 25px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4); }
27569ccd446eSAtari911            }
27579ccd446eSAtari911
27589ccd446eSAtari911            @keyframes shimmer-' . $calId . ' {
27599ccd446eSAtari911                0% { background-position: -200% center; }
27609ccd446eSAtari911                100% { background-position: 200% center; }
27619ccd446eSAtari911            }
27629ccd446eSAtari911
27639ccd446eSAtari911            .sidebar-pink {
27649ccd446eSAtari911                animation: pulse-glow-' . $calId . ' 3s ease-in-out infinite;
27659ccd446eSAtari911            }
27669ccd446eSAtari911
27679ccd446eSAtari911            .sidebar-pink:hover {
27689ccd446eSAtari911                box-shadow: 0 0 30px rgba(255, 20, 147, 0.9), 0 0 50px rgba(255, 20, 147, 0.5) !important;
27699ccd446eSAtari911            }
27709ccd446eSAtari911
27719ccd446eSAtari911            .sparkle-' . $calId . ' {
27729ccd446eSAtari911                position: absolute;
27739ccd446eSAtari911                pointer-events: none;
27749ccd446eSAtari911                font-size: 20px;
27759ccd446eSAtari911                z-index: 1000;
27769ccd446eSAtari911                animation: sparkle-' . $calId . ' 1s ease-out forwards;
27779ccd446eSAtari911                filter: drop-shadow(0 0 3px rgba(255, 20, 147, 0.8));
27789ccd446eSAtari911            }
27799ccd446eSAtari911            </style>';
27809ccd446eSAtari911
27819ccd446eSAtari911            $html .= '<script>
27829ccd446eSAtari911            (function() {
27839ccd446eSAtari911                const container = document.getElementById("sidebar-widget-' . $calId . '");
27849ccd446eSAtari911                const sparkles = ["✨", "��", "��", "⭐", "��", "��", "��", "��", "��", "��"];
27859ccd446eSAtari911
27869ccd446eSAtari911                function createSparkle(x, y) {
27879ccd446eSAtari911                    const sparkle = document.createElement("div");
27889ccd446eSAtari911                    sparkle.className = "sparkle-' . $calId . '";
27899ccd446eSAtari911                    sparkle.textContent = sparkles[Math.floor(Math.random() * sparkles.length)];
27909ccd446eSAtari911                    sparkle.style.left = x + "px";
27919ccd446eSAtari911                    sparkle.style.top = y + "px";
27929ccd446eSAtari911
27939ccd446eSAtari911                    // Random direction
27949ccd446eSAtari911                    const angle = Math.random() * Math.PI * 2;
27959ccd446eSAtari911                    const distance = 30 + Math.random() * 40;
27969ccd446eSAtari911                    sparkle.style.setProperty("--tx", Math.cos(angle) * distance + "px");
27979ccd446eSAtari911                    sparkle.style.setProperty("--ty", Math.sin(angle) * distance + "px");
27989ccd446eSAtari911
27999ccd446eSAtari911                    container.appendChild(sparkle);
28009ccd446eSAtari911
28019ccd446eSAtari911                    setTimeout(() => sparkle.remove(), 1000);
28029ccd446eSAtari911                }
28039ccd446eSAtari911
28049ccd446eSAtari911                // Click sparkles
28059ccd446eSAtari911                container.addEventListener("click", function(e) {
28069ccd446eSAtari911                    const rect = container.getBoundingClientRect();
28079ccd446eSAtari911                    const x = e.clientX - rect.left;
28089ccd446eSAtari911                    const y = e.clientY - rect.top;
28099ccd446eSAtari911
28109ccd446eSAtari911                    // Create LOTS of sparkles for maximum bling!
28119ccd446eSAtari911                    for (let i = 0; i < 8; i++) {
28129ccd446eSAtari911                        setTimeout(() => {
28139ccd446eSAtari911                            const offsetX = x + (Math.random() - 0.5) * 30;
28149ccd446eSAtari911                            const offsetY = y + (Math.random() - 0.5) * 30;
28159ccd446eSAtari911                            createSparkle(offsetX, offsetY);
28169ccd446eSAtari911                        }, i * 40);
28179ccd446eSAtari911                    }
28189ccd446eSAtari911                });
28199ccd446eSAtari911
28209ccd446eSAtari911                // Random auto-sparkles for extra glamour
28219ccd446eSAtari911                setInterval(() => {
28229ccd446eSAtari911                    const x = Math.random() * container.offsetWidth;
28239ccd446eSAtari911                    const y = Math.random() * container.offsetHeight;
28249ccd446eSAtari911                    createSparkle(x, y);
28259ccd446eSAtari911                }, 3000);
28269ccd446eSAtari911            })();
28279ccd446eSAtari911            </script>';
28289ccd446eSAtari911        }
28291d05cddcSAtari911
28301d05cddcSAtari911        // Sanitize calId for use in JavaScript variable names (remove dashes)
28311d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
28321d05cddcSAtari911
28331d05cddcSAtari911        // CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it
28341d05cddcSAtari911        $html .= '<script>
28351d05cddcSAtari911(function() {
28361d05cddcSAtari911    // Shared state for system stats and tooltips
28371d05cddcSAtari911    const sharedState_' . $jsCalId . ' = {
28381d05cddcSAtari911        latestStats: {
28391d05cddcSAtari911            load: {"1min": 0, "5min": 0, "15min": 0},
28401d05cddcSAtari911            uptime: "",
28411d05cddcSAtari911            memory_details: {},
28421d05cddcSAtari911            top_processes: []
28431d05cddcSAtari911        },
28441d05cddcSAtari911        cpuHistory: [],
28451d05cddcSAtari911        CPU_HISTORY_SIZE: 2
28461d05cddcSAtari911    };
28471d05cddcSAtari911
28481d05cddcSAtari911    // Tooltip functions - MUST be defined before HTML uses them
28491d05cddcSAtari911    window["showTooltip_' . $jsCalId . '"] = function(color) {
28501d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
28511d05cddcSAtari911        if (!tooltip) {
28521d05cddcSAtari911            console.log("Tooltip element not found for color:", color);
28531d05cddcSAtari911            return;
28541d05cddcSAtari911        }
28551d05cddcSAtari911
28561d05cddcSAtari911        const latestStats = sharedState_' . $jsCalId . '.latestStats;
28571d05cddcSAtari911        let content = "";
28581d05cddcSAtari911
28591d05cddcSAtari911        if (color === "green") {
28601d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">CPU Load Average</div>";
28611d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
28621d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
28631d05cddcSAtari911            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
28641d05cddcSAtari911            if (latestStats.uptime) {
28657e8ea635SAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_bright'] . ';\\">Uptime: " + latestStats.uptime + "</div>";
28661d05cddcSAtari911            }
28677e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['text_bright'] . '", "important");
28687e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['text_bright'] . '", "important");
28697e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_bright'] . '", "important");
28701d05cddcSAtari911        } else if (color === "purple") {
28711d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">CPU Load (Short-term)</div>";
28721d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
28731d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
28741d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
28757e8ea635SAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['border'] . ';\\" class=\\"tooltip-title\\">Top Processes</div>";
28761d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
28771d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
28781d05cddcSAtari911                });
28791d05cddcSAtari911            }
28807e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['border'] . '", "important");
28817e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['border'] . '", "important");
28827e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['border'] . '", "important");
28831d05cddcSAtari911        } else if (color === "orange") {
28841d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">Memory Usage</div>";
28851d05cddcSAtari911            if (latestStats.memory_details && latestStats.memory_details.total) {
28861d05cddcSAtari911                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
28871d05cddcSAtari911                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
28881d05cddcSAtari911                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
28891d05cddcSAtari911                if (latestStats.memory_details.cached) {
28901d05cddcSAtari911                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
28911d05cddcSAtari911                }
28921d05cddcSAtari911            } else {
28931d05cddcSAtari911                content += "<div>Loading...</div>";
28941d05cddcSAtari911            }
28951d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
28967e8ea635SAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_primary'] . ';\\" class=\\"tooltip-title\\">Top Processes</div>";
28971d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
28981d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
28991d05cddcSAtari911                });
29001d05cddcSAtari911            }
29017e8ea635SAtari911            tooltip.style.setProperty("border-color", "' . $themeStyles['text_primary'] . '", "important");
29027e8ea635SAtari911            tooltip.style.setProperty("color", "' . $themeStyles['text_primary'] . '", "important");
29037e8ea635SAtari911            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_primary'] . '", "important");
29041d05cddcSAtari911        }
29051d05cddcSAtari911
29061d05cddcSAtari911        tooltip.innerHTML = content;
29077e8ea635SAtari911        tooltip.style.setProperty("display", "block");
29087e8ea635SAtari911        tooltip.style.setProperty("background", "' . $themeStyles['bg'] . '", "important");
29091d05cddcSAtari911
29101d05cddcSAtari911        const bar = tooltip.parentElement;
29111d05cddcSAtari911        const barRect = bar.getBoundingClientRect();
29121d05cddcSAtari911        const tooltipRect = tooltip.getBoundingClientRect();
29131d05cddcSAtari911
29141d05cddcSAtari911        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
29151d05cddcSAtari911        const top = barRect.top - tooltipRect.height - 8;
29161d05cddcSAtari911
29171d05cddcSAtari911        tooltip.style.left = left + "px";
29181d05cddcSAtari911        tooltip.style.top = top + "px";
29191d05cddcSAtari911    };
29201d05cddcSAtari911
29211d05cddcSAtari911    window["hideTooltip_' . $jsCalId . '"] = function(color) {
29221d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
29231d05cddcSAtari911        if (tooltip) {
29241d05cddcSAtari911            tooltip.style.display = "none";
29251d05cddcSAtari911        }
29261d05cddcSAtari911    };
29271d05cddcSAtari911
29281d05cddcSAtari911    // Update clock every second
29291d05cddcSAtari911    function updateClock() {
29301d05cddcSAtari911        const now = new Date();
29311d05cddcSAtari911        let hours = now.getHours();
29321d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
29331d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
29341d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
29351d05cddcSAtari911        hours = hours % 12 || 12;
29361d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
29371d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
29381d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
29391d05cddcSAtari911    }
29401d05cddcSAtari911    setInterval(updateClock, 1000);
29411d05cddcSAtari911
294296df7d3eSAtari911    // Weather - uses default location, click weather to get local
294396df7d3eSAtari911    var userLocationGranted = false;
294496df7d3eSAtari911    var userLat = 38.5816;  // Sacramento default
294596df7d3eSAtari911    var userLon = -121.4944;
29461d05cddcSAtari911
294796df7d3eSAtari911    function fetchWeatherData(lat, lon) {
294896df7d3eSAtari911        fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_weather=true&temperature_unit=fahrenheit")
29491d05cddcSAtari911            .then(response => response.json())
29501d05cddcSAtari911            .then(data => {
29511d05cddcSAtari911                if (data.current_weather) {
29521d05cddcSAtari911                    const temp = Math.round(data.current_weather.temperature);
29531d05cddcSAtari911                    const weatherCode = data.current_weather.weathercode;
29541d05cddcSAtari911                    const icon = getWeatherIcon(weatherCode);
29551d05cddcSAtari911                    const iconEl = document.getElementById("weather-icon-' . $calId . '");
29561d05cddcSAtari911                    const tempEl = document.getElementById("weather-temp-' . $calId . '");
29571d05cddcSAtari911                    if (iconEl) iconEl.textContent = icon;
29581d05cddcSAtari911                    if (tempEl) tempEl.innerHTML = temp + "&deg;";
29591d05cddcSAtari911                }
29601d05cddcSAtari911            })
29611d05cddcSAtari911            .catch(error => console.log("Weather fetch error:", error));
296296df7d3eSAtari911    }
296396df7d3eSAtari911
296496df7d3eSAtari911    function updateWeather() {
296596df7d3eSAtari911        fetchWeatherData(userLat, userLon);
296696df7d3eSAtari911    }
296796df7d3eSAtari911
296896df7d3eSAtari911    // Click weather icon to request local weather (user gesture required)
296996df7d3eSAtari911    function requestLocalWeather() {
297096df7d3eSAtari911        if (userLocationGranted) return;
297196df7d3eSAtari911        if ("geolocation" in navigator) {
297296df7d3eSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
297396df7d3eSAtari911                userLat = position.coords.latitude;
297496df7d3eSAtari911                userLon = position.coords.longitude;
297596df7d3eSAtari911                userLocationGranted = true;
297696df7d3eSAtari911                fetchWeatherData(userLat, userLon);
29771d05cddcSAtari911            }, function(error) {
297896df7d3eSAtari911                console.log("Geolocation denied, using default location");
29791d05cddcSAtari911            });
29801d05cddcSAtari911        }
29811d05cddcSAtari911    }
29821d05cddcSAtari911
298396df7d3eSAtari911    setTimeout(function() {
298496df7d3eSAtari911        var weatherEl = document.querySelector("#weather-icon-' . $calId . '");
298596df7d3eSAtari911        if (weatherEl) {
298696df7d3eSAtari911            weatherEl.style.cursor = "pointer";
298796df7d3eSAtari911            weatherEl.title = "Click for local weather";
298896df7d3eSAtari911            weatherEl.addEventListener("click", requestLocalWeather);
298996df7d3eSAtari911        }
299096df7d3eSAtari911    }, 100);
299196df7d3eSAtari911
29921d05cddcSAtari911    function getWeatherIcon(code) {
29931d05cddcSAtari911        const icons = {
29941d05cddcSAtari911            0: "☀️", 1: "��️", 2: "⛅", 3: "☁️",
29951d05cddcSAtari911            45: "��️", 48: "��️", 51: "��️", 53: "��️", 55: "��️",
29961d05cddcSAtari911            61: "��️", 63: "��️", 65: "⛈️", 71: "��️", 73: "��️",
29971d05cddcSAtari911            75: "❄️", 77: "��️", 80: "��️", 81: "��️", 82: "⛈️",
29981d05cddcSAtari911            85: "��️", 86: "❄️", 95: "⛈️", 96: "⛈️", 99: "⛈️"
29991d05cddcSAtari911        };
30001d05cddcSAtari911        return icons[code] || "��️";
30011d05cddcSAtari911    }
30021d05cddcSAtari911
30031d05cddcSAtari911    // Update weather immediately and every 10 minutes
30041d05cddcSAtari911    updateWeather();
30051d05cddcSAtari911    setInterval(updateWeather, 600000);
30061d05cddcSAtari911
30071d05cddcSAtari911    // Update system stats and tooltips data
30081d05cddcSAtari911    function updateSystemStats() {
30091d05cddcSAtari911        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
30101d05cddcSAtari911            .then(response => response.json())
30111d05cddcSAtari911            .then(data => {
30121d05cddcSAtari911                sharedState_' . $jsCalId . '.latestStats = {
30131d05cddcSAtari911                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
30141d05cddcSAtari911                    uptime: data.uptime || "",
30151d05cddcSAtari911                    memory_details: data.memory_details || {},
30161d05cddcSAtari911                    top_processes: data.top_processes || []
30171d05cddcSAtari911                };
30181d05cddcSAtari911
30191d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
30201d05cddcSAtari911                if (greenBar) {
30211d05cddcSAtari911                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
30221d05cddcSAtari911                }
30231d05cddcSAtari911
30241d05cddcSAtari911                sharedState_' . $jsCalId . '.cpuHistory.push(data.cpu);
30251d05cddcSAtari911                if (sharedState_' . $jsCalId . '.cpuHistory.length > sharedState_' . $jsCalId . '.CPU_HISTORY_SIZE) {
30261d05cddcSAtari911                    sharedState_' . $jsCalId . '.cpuHistory.shift();
30271d05cddcSAtari911                }
30281d05cddcSAtari911
30291d05cddcSAtari911                const cpuAverage = sharedState_' . $jsCalId . '.cpuHistory.reduce((sum, val) => sum + val, 0) / sharedState_' . $jsCalId . '.cpuHistory.length;
30301d05cddcSAtari911
30311d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
30321d05cddcSAtari911                if (cpuBar) {
30331d05cddcSAtari911                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
30341d05cddcSAtari911                }
30351d05cddcSAtari911
30361d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
30371d05cddcSAtari911                if (memBar) {
30381d05cddcSAtari911                    memBar.style.width = Math.min(100, data.memory) + "%";
30391d05cddcSAtari911                }
30401d05cddcSAtari911            })
30411d05cddcSAtari911            .catch(error => {
30421d05cddcSAtari911                console.log("System stats error:", error);
30431d05cddcSAtari911            });
30441d05cddcSAtari911    }
30451d05cddcSAtari911
30461d05cddcSAtari911    updateSystemStats();
30471d05cddcSAtari911    setInterval(updateSystemStats, 2000);
30481d05cddcSAtari911})();
30491d05cddcSAtari911</script>';
30501d05cddcSAtari911
30511d05cddcSAtari911        // NOW add the header HTML (after JavaScript is defined)
30521d05cddcSAtari911        $todayDate = new DateTime();
30531d05cddcSAtari911        $displayDate = $todayDate->format('D, M j, Y');
30541d05cddcSAtari911        $currentTime = $todayDate->format('g:i:s A');
30551d05cddcSAtari911
30569ccd446eSAtari911        $html .= '<div class="eventlist-today-header" style="background:' . $themeStyles['header_bg'] . '; border:2px solid ' . $themeStyles['header_border'] . '; box-shadow:' . $themeStyles['header_shadow'] . ';">';
30579ccd446eSAtari911        $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '" style="color:' . $themeStyles['text_bright'] . ';">' . $currentTime . '</span>';
30581d05cddcSAtari911        $html .= '<div class="eventlist-bottom-info">';
30599ccd446eSAtari911        $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '" style="color:' . $themeStyles['text_primary'] . ';">--°</span></span>';
30609ccd446eSAtari911        $html .= '<span class="eventlist-today-date" style="color:' . $themeStyles['text_dim'] . ';">' . $displayDate . '</span>';
30611d05cddcSAtari911        $html .= '</div>';
30621d05cddcSAtari911
30631d05cddcSAtari911        // Three CPU/Memory bars (all update live)
30641d05cddcSAtari911        $html .= '<div class="eventlist-stats-container">';
30651d05cddcSAtari911
30661d05cddcSAtari911        // 5-minute load average (green, updates every 2 seconds)
30677e8ea635SAtari911        $html .= '<div class="eventlist-cpu-bar" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $jsCalId . '(\'green\')" onmouseout="hideTooltip_' . $jsCalId . '(\'green\')">';
30687e8ea635SAtari911        $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_bright'] . ' !important;"></div>';
30691d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
30701d05cddcSAtari911        $html .= '</div>';
30711d05cddcSAtari911
30721d05cddcSAtari911        // Real-time CPU (purple, updates with 5-sec average)
30737e8ea635SAtari911        $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $jsCalId . '(\'purple\')" onmouseout="hideTooltip_' . $jsCalId . '(\'purple\')">';
30747e8ea635SAtari911        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['border'] . ' !important;"></div>';
30751d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
30761d05cddcSAtari911        $html .= '</div>';
30771d05cddcSAtari911
30781d05cddcSAtari911        // Real-time Memory (orange, updates)
30797e8ea635SAtari911        $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $jsCalId . '(\'orange\')" onmouseout="hideTooltip_' . $jsCalId . '(\'orange\')">';
30807e8ea635SAtari911        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_primary'] . ' !important;"></div>';
30811d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
30821d05cddcSAtari911        $html .= '</div>';
30831d05cddcSAtari911
30841d05cddcSAtari911        $html .= '</div>';
30851d05cddcSAtari911        $html .= '</div>';
30861d05cddcSAtari911
3087231d0edbSAtari911        // Get today's date for default event date
3088231d0edbSAtari911        $todayStr = date('Y-m-d');
3089231d0edbSAtari911
30909ccd446eSAtari911        // Thin "Add Event" bar between header and week grid - theme-aware colors
30917e8ea635SAtari911        $addBtnBg = $themeStyles['cell_today_bg'];
30927e8ea635SAtari911        $addBtnHover = $themeStyles['grid_bg'];
30937e8ea635SAtari911        $addBtnTextColor = ($theme === 'professional' || $theme === 'wiki') ?
30947e8ea635SAtari911                          $themeStyles['text_bright'] : $themeStyles['text_bright'];
30957e8ea635SAtari911        $addBtnShadow = ($theme === 'professional' || $theme === 'wiki') ?
30967e8ea635SAtari911                       '0 2px 4px rgba(0,0,0,0.2)' : '0 0 8px ' . $themeStyles['shadow'];
30977e8ea635SAtari911        $addBtnHoverShadow = ($theme === 'professional' || $theme === 'wiki') ?
30987e8ea635SAtari911                            '0 3px 6px rgba(0,0,0,0.3)' : '0 0 12px ' . $themeStyles['shadow'];
30999ccd446eSAtari911
31009ccd446eSAtari911        $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 . '\';">';
31019ccd446eSAtari911        $addBtnTextShadow = ($theme === 'pink') ? '0 0 3px ' . $addBtnTextColor : 'none';
3102*da206178SAtari911        $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>';
31031d05cddcSAtari911        $html .= '</div>';
31041d05cddcSAtari911
31051d05cddcSAtari911        // Week grid (7 cells)
31069ccd446eSAtari911        $html .= $this->renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme);
31071d05cddcSAtari911
31087e8ea635SAtari911        // Section colors - derived from theme palette
31097e8ea635SAtari911        // Today: brightest accent, Tomorrow: primary accent, Important: dim/secondary accent
31107e8ea635SAtari911        if ($theme === 'matrix') {
31117e8ea635SAtari911            $todayColor = '#00ff00';     // Bright green
31127e8ea635SAtari911            $tomorrowColor = '#00cc07';  // Standard green
31137e8ea635SAtari911            $importantColor = '#00aa00'; // Dim green
31147e8ea635SAtari911        } else if ($theme === 'purple') {
31157e8ea635SAtari911            $todayColor = '#d4a5ff';     // Bright purple
31167e8ea635SAtari911            $tomorrowColor = '#9b59b6';  // Standard purple
31177e8ea635SAtari911            $importantColor = '#8e7ab8'; // Dim purple
31187e8ea635SAtari911        } else if ($theme === 'pink') {
31197e8ea635SAtari911            $todayColor = '#ff1493';     // Hot pink
31207e8ea635SAtari911            $tomorrowColor = '#ff69b4';  // Medium pink
31217e8ea635SAtari911            $importantColor = '#ff85c1'; // Light pink
31227e8ea635SAtari911        } else if ($theme === 'professional') {
31237e8ea635SAtari911            $todayColor = '#4a90e2';     // Blue accent
31247e8ea635SAtari911            $tomorrowColor = '#5ba3e6';  // Lighter blue
31257e8ea635SAtari911            $importantColor = '#7fb8ec'; // Lightest blue
31269ccd446eSAtari911        } else {
31277e8ea635SAtari911            // Wiki - section header backgrounds from template colors
31287e8ea635SAtari911            $todayColor = $themeStyles['text_bright'];      // __link__
31297e8ea635SAtari911            $tomorrowColor = $themeStyles['header_bg'];     // __background_alt__
31307e8ea635SAtari911            $importantColor = $themeStyles['header_border'];// __border__
31319ccd446eSAtari911        }
31329ccd446eSAtari911
313396df7d3eSAtari911        // Check if there are any itinerary items
313496df7d3eSAtari911        $hasItinerary = !empty($todayEvents) || !empty($tomorrowEvents) || !empty($importantEvents);
313596df7d3eSAtari911
313696df7d3eSAtari911        // Itinerary bar (collapsible toggle) - styled like +Add bar
313796df7d3eSAtari911        $itineraryBg = $themeStyles['cell_today_bg'];
313896df7d3eSAtari911        $itineraryHover = $themeStyles['grid_bg'];
313996df7d3eSAtari911        $itineraryTextColor = ($theme === 'professional' || $theme === 'wiki') ?
314096df7d3eSAtari911                              $themeStyles['text_bright'] : $themeStyles['text_bright'];
314196df7d3eSAtari911        $itineraryShadow = ($theme === 'professional' || $theme === 'wiki') ?
314296df7d3eSAtari911                           '0 2px 4px rgba(0,0,0,0.2)' : '0 0 8px ' . $themeStyles['shadow'];
314396df7d3eSAtari911        $itineraryHoverShadow = ($theme === 'professional' || $theme === 'wiki') ?
314496df7d3eSAtari911                                '0 3px 6px rgba(0,0,0,0.3)' : '0 0 12px ' . $themeStyles['shadow'];
314596df7d3eSAtari911        $itineraryTextShadow = ($theme === 'pink') ? '0 0 3px ' . $itineraryTextColor : 'none';
314696df7d3eSAtari911
314796df7d3eSAtari911        // Sanitize calId for JavaScript
314896df7d3eSAtari911        $jsCalId = str_replace('-', '_', $calId);
314996df7d3eSAtari911
315096df7d3eSAtari911        // Get itinerary default state from settings
315196df7d3eSAtari911        $itineraryDefaultCollapsed = $this->getItineraryCollapsed();
315296df7d3eSAtari911        $arrowDefaultStyle = $itineraryDefaultCollapsed ? 'transform:rotate(-90deg);' : '';
315396df7d3eSAtari911        $contentDefaultStyle = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : '';
315496df7d3eSAtari911
315596df7d3eSAtari911        $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 . '\';">';
315696df7d3eSAtari911        $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>';
315796df7d3eSAtari911        $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>';
315896df7d3eSAtari911        $html .= '</div>';
315996df7d3eSAtari911
316096df7d3eSAtari911        // Itinerary content container (collapsible)
316196df7d3eSAtari911        $html .= '<div id="itinerary-content-' . $calId . '" style="transition:max-height 0.3s ease-out, opacity 0.2s ease-out; overflow:hidden; ' . $contentDefaultStyle . '">';
316296df7d3eSAtari911
31639ccd446eSAtari911        // Today section
31641d05cddcSAtari911        if (!empty($todayEvents)) {
3165*da206178SAtari911            $html .= $this->renderSidebarSection('Today', $todayEvents, $todayColor, $calId, $themeStyles, $theme, $importantNsList);
31661d05cddcSAtari911        }
31671d05cddcSAtari911
31689ccd446eSAtari911        // Tomorrow section
31691d05cddcSAtari911        if (!empty($tomorrowEvents)) {
3170*da206178SAtari911            $html .= $this->renderSidebarSection('Tomorrow', $tomorrowEvents, $tomorrowColor, $calId, $themeStyles, $theme, $importantNsList);
31711d05cddcSAtari911        }
31721d05cddcSAtari911
31739ccd446eSAtari911        // Important events section
31741d05cddcSAtari911        if (!empty($importantEvents)) {
3175*da206178SAtari911            $html .= $this->renderSidebarSection('Important Events', $importantEvents, $importantColor, $calId, $themeStyles, $theme, $importantNsList);
31761d05cddcSAtari911        }
31771d05cddcSAtari911
317896df7d3eSAtari911        // Empty state if no itinerary items
317996df7d3eSAtari911        if (!$hasItinerary) {
3180*da206178SAtari911            $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>';
318196df7d3eSAtari911        }
318296df7d3eSAtari911
318396df7d3eSAtari911        $html .= '</div>'; // Close itinerary-content
318496df7d3eSAtari911
318596df7d3eSAtari911        // Get itinerary default state from settings
318696df7d3eSAtari911        $itineraryDefaultCollapsed = $this->getItineraryCollapsed();
318796df7d3eSAtari911        $itineraryExpandedDefault = $itineraryDefaultCollapsed ? 'false' : 'true';
318896df7d3eSAtari911        $itineraryArrowDefault = $itineraryDefaultCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
318996df7d3eSAtari911        $itineraryContentDefault = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : 'max-height:none;';
319096df7d3eSAtari911
319196df7d3eSAtari911        // JavaScript for toggling itinerary
319296df7d3eSAtari911        $html .= '<script>
319396df7d3eSAtari911        (function() {
319496df7d3eSAtari911            let itineraryExpanded_' . $jsCalId . ' = ' . $itineraryExpandedDefault . ';
319596df7d3eSAtari911
319696df7d3eSAtari911            window.toggleItinerary_' . $jsCalId . ' = function() {
319796df7d3eSAtari911                const content = document.getElementById("itinerary-content-' . $calId . '");
319896df7d3eSAtari911                const arrow = document.getElementById("itinerary-arrow-' . $calId . '");
319996df7d3eSAtari911
320096df7d3eSAtari911                if (itineraryExpanded_' . $jsCalId . ') {
320196df7d3eSAtari911                    // Collapse
320296df7d3eSAtari911                    content.style.maxHeight = "0px";
320396df7d3eSAtari911                    content.style.opacity = "0";
320496df7d3eSAtari911                    arrow.style.transform = "rotate(-90deg)";
320596df7d3eSAtari911                    itineraryExpanded_' . $jsCalId . ' = false;
320696df7d3eSAtari911                } else {
320796df7d3eSAtari911                    // Expand
320896df7d3eSAtari911                    content.style.maxHeight = content.scrollHeight + "px";
320996df7d3eSAtari911                    content.style.opacity = "1";
321096df7d3eSAtari911                    arrow.style.transform = "rotate(0deg)";
321196df7d3eSAtari911                    itineraryExpanded_' . $jsCalId . ' = true;
321296df7d3eSAtari911
321396df7d3eSAtari911                    // After transition, set to auto for dynamic content
321496df7d3eSAtari911                    setTimeout(function() {
321596df7d3eSAtari911                        if (itineraryExpanded_' . $jsCalId . ') {
321696df7d3eSAtari911                            content.style.maxHeight = "none";
321796df7d3eSAtari911                        }
321896df7d3eSAtari911                    }, 300);
321996df7d3eSAtari911                }
322096df7d3eSAtari911            };
322196df7d3eSAtari911
322296df7d3eSAtari911            // Initialize based on default state
322396df7d3eSAtari911            const content = document.getElementById("itinerary-content-' . $calId . '");
322496df7d3eSAtari911            const arrow = document.getElementById("itinerary-arrow-' . $calId . '");
322596df7d3eSAtari911            if (content && arrow) {
322696df7d3eSAtari911                if (' . $itineraryExpandedDefault . ') {
322796df7d3eSAtari911                    content.style.maxHeight = "none";
322896df7d3eSAtari911                    arrow.style.transform = "rotate(0deg)";
322996df7d3eSAtari911                } else {
323096df7d3eSAtari911                    content.style.maxHeight = "0px";
323196df7d3eSAtari911                    content.style.opacity = "0";
323296df7d3eSAtari911                    arrow.style.transform = "rotate(-90deg)";
323396df7d3eSAtari911                }
323496df7d3eSAtari911            }
323596df7d3eSAtari911        })();
323696df7d3eSAtari911        </script>';
323796df7d3eSAtari911
32381d05cddcSAtari911        $html .= '</div>';
32391d05cddcSAtari911
3240231d0edbSAtari911        // Add event dialog for sidebar widget
32410c3b6e81SAtari911        $html .= $this->renderEventDialog($calId, $namespace, $theme);
3242231d0edbSAtari911
32439ccd446eSAtari911        // Add JavaScript for positioning data-tooltip elements
32449ccd446eSAtari911        $html .= '<script>
32459ccd446eSAtari911        // Position data-tooltip elements to prevent cutoff (up and to the LEFT)
32469ccd446eSAtari911        document.addEventListener("DOMContentLoaded", function() {
32479ccd446eSAtari911            const tooltipElements = document.querySelectorAll("[data-tooltip]");
32489ccd446eSAtari911            const isPinkTheme = document.querySelector(".sidebar-pink") !== null;
32499ccd446eSAtari911
32509ccd446eSAtari911            tooltipElements.forEach(function(element) {
32519ccd446eSAtari911                element.addEventListener("mouseenter", function() {
32529ccd446eSAtari911                    const rect = element.getBoundingClientRect();
32539ccd446eSAtari911                    const style = window.getComputedStyle(element, ":before");
32549ccd446eSAtari911
32559ccd446eSAtari911                    // Position above the element, aligned to LEFT (not right)
32569ccd446eSAtari911                    element.style.setProperty("--tooltip-left", (rect.left - 150) + "px");
32579ccd446eSAtari911                    element.style.setProperty("--tooltip-top", (rect.top - 30) + "px");
32589ccd446eSAtari911
32599ccd446eSAtari911                    // Pink theme: position heart to the right of tooltip
32609ccd446eSAtari911                    if (isPinkTheme) {
32619ccd446eSAtari911                        element.style.setProperty("--heart-left", (rect.left - 150 + 210) + "px");
32629ccd446eSAtari911                        element.style.setProperty("--heart-top", (rect.top - 30) + "px");
32639ccd446eSAtari911                    }
32649ccd446eSAtari911                });
32659ccd446eSAtari911            });
32669ccd446eSAtari911        });
32679ccd446eSAtari911
32689ccd446eSAtari911        // Apply custom properties to position tooltips
32699ccd446eSAtari911        const style = document.createElement("style");
32709ccd446eSAtari911        style.textContent = `
32719ccd446eSAtari911            [data-tooltip]:hover:before {
32729ccd446eSAtari911                left: var(--tooltip-left, 0) !important;
32739ccd446eSAtari911                top: var(--tooltip-top, 0) !important;
32749ccd446eSAtari911            }
32759ccd446eSAtari911            .sidebar-pink [data-tooltip]:hover:after {
32769ccd446eSAtari911                left: var(--heart-left, 0) !important;
32779ccd446eSAtari911                top: var(--heart-top, 0) !important;
32789ccd446eSAtari911            }
32799ccd446eSAtari911        `;
32809ccd446eSAtari911        document.head.appendChild(style);
32819ccd446eSAtari911        </script>';
32829ccd446eSAtari911
32831d05cddcSAtari911        return $html;
32841d05cddcSAtari911    }
32851d05cddcSAtari911
32861d05cddcSAtari911    /**
32879ccd446eSAtari911     * Render compact week grid (7 cells with event bars) - Theme-aware
32881d05cddcSAtari911     */
32899ccd446eSAtari911    private function renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme) {
32901d05cddcSAtari911        // Generate unique ID for this calendar instance - sanitize for JavaScript
32911d05cddcSAtari911        $calId = 'cal_' . substr(md5($weekStart . microtime()), 0, 8);
32921d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);  // Sanitize for JS variable names
32931d05cddcSAtari911
32949ccd446eSAtari911        $html = '<div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:' . $themeStyles['grid_bg'] . '; border-bottom:2px solid ' . $themeStyles['grid_border'] . ';">';
32951d05cddcSAtari911
32969ccd446eSAtari911        // Day names depend on week start setting
32979ccd446eSAtari911        $weekStartDay = $this->getWeekStartDay();
32989ccd446eSAtari911        if ($weekStartDay === 'monday') {
32999ccd446eSAtari911            $dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];  // Monday to Sunday
33009ccd446eSAtari911        } else {
33019ccd446eSAtari911            $dayNames = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];  // Sunday to Saturday
33029ccd446eSAtari911        }
33031d05cddcSAtari911        $today = date('Y-m-d');
33041d05cddcSAtari911
33051d05cddcSAtari911        for ($i = 0; $i < 7; $i++) {
33061d05cddcSAtari911            $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days'));
33071d05cddcSAtari911            $dayNum = date('j', strtotime($date));
33081d05cddcSAtari911            $isToday = $date === $today;
33091d05cddcSAtari911
33101d05cddcSAtari911            $events = isset($weekEvents[$date]) ? $weekEvents[$date] : [];
33111d05cddcSAtari911            $eventCount = count($events);
33121d05cddcSAtari911
33139ccd446eSAtari911            $bgColor = $isToday ? $themeStyles['cell_today_bg'] : $themeStyles['cell_bg'];
33149ccd446eSAtari911            $textColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
33151d05cddcSAtari911            $fontWeight = $isToday ? '700' : '500';
33169ccd446eSAtari911
33179ccd446eSAtari911            // Theme-aware text shadow
33189ccd446eSAtari911            if ($theme === 'pink') {
33199ccd446eSAtari911                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
33207e8ea635SAtari911                $textShadow = $isToday ? 'text-shadow:0 0 3px ' . $glowColor . ';' : 'text-shadow:0 0 2px ' . $glowColor . ';';
33217e8ea635SAtari911            } else if ($theme === 'matrix') {
33227e8ea635SAtari911                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
33237e8ea635SAtari911                $textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glowColor . ';';
33247e8ea635SAtari911            } else if ($theme === 'purple') {
33257e8ea635SAtari911                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
33267e8ea635SAtari911                $textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glowColor . ';';
33279ccd446eSAtari911            } else {
33287e8ea635SAtari911                $textShadow = '';  // No glow for professional/wiki
33299ccd446eSAtari911            }
33309ccd446eSAtari911
33319ccd446eSAtari911            // Border color based on theme
33329ccd446eSAtari911            $borderColor = $themeStyles['grid_border'];
33331d05cddcSAtari911
33341d05cddcSAtari911            $hasEvents = $eventCount > 0;
33351d05cddcSAtari911            $clickableStyle = $hasEvents ? 'cursor:pointer;' : '';
33361d05cddcSAtari911            $clickHandler = $hasEvents ? ' onclick="showDayEvents_' . $jsCalId . '(\'' . $date . '\')"' : '';
33371d05cddcSAtari911
33389ccd446eSAtari911            $html .= '<div style="background:' . $bgColor . '; padding:4px 2px; text-align:center; min-height:45px; position:relative; border:1px solid ' . $borderColor . ' !important; ' . $clickableStyle . '" ' . $clickHandler . '>';
33391d05cddcSAtari911
33409ccd446eSAtari911            // Day letter - theme color
33419ccd446eSAtari911            $dayLetterColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary'];
33429ccd446eSAtari911            $html .= '<div style="font-size:9px; color:' . $dayLetterColor . '; font-weight:500; font-family:system-ui, sans-serif;">' . $dayNames[$i] . '</div>';
33431d05cddcSAtari911
33441d05cddcSAtari911            // Day number
33451d05cddcSAtari911            $html .= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:2px 0; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNum . '</div>';
33461d05cddcSAtari911
33479ccd446eSAtari911            // Event bars (max 4 visible) with theme-aware glow
33481d05cddcSAtari911            if ($eventCount > 0) {
33499ccd446eSAtari911                $showCount = min($eventCount, 4);
33501d05cddcSAtari911                for ($j = 0; $j < $showCount; $j++) {
33511d05cddcSAtari911                    $event = $events[$j];
33529ccd446eSAtari911                    $color = isset($event['color']) ? $event['color'] : $themeStyles['text_primary'];
33539ccd446eSAtari911                    $barShadow = $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . htmlspecialchars($color);
33549ccd446eSAtari911                    $html .= '<div style="height:2px; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:' . $barShadow . ';"></div>';
33551d05cddcSAtari911                }
33561d05cddcSAtari911
33579ccd446eSAtari911                // Show "+N more" if more than 4 - theme color
33589ccd446eSAtari911                if ($eventCount > 4) {
33599ccd446eSAtari911                    $moreTextColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary'];
33609ccd446eSAtari911                    $html .= '<div style="font-size:7px; color:' . $moreTextColor . '; margin-top:1px; font-family:system-ui, sans-serif;">+' . ($eventCount - 4) . '</div>';
33611d05cddcSAtari911                }
33621d05cddcSAtari911            }
33631d05cddcSAtari911
33641d05cddcSAtari911            $html .= '</div>';
33651d05cddcSAtari911        }
33661d05cddcSAtari911
33671d05cddcSAtari911        $html .= '</div>';
33681d05cddcSAtari911
33699ccd446eSAtari911        // Add container for selected day events display (with unique ID) - theme-aware
33707e8ea635SAtari911        $panelBorderColor = $themeStyles['border'];
33717e8ea635SAtari911        $panelHeaderBg = $themeStyles['border'];
33727e8ea635SAtari911        $panelShadow = ($theme === 'professional' || $theme === 'wiki') ?
33737e8ea635SAtari911                      '0 1px 3px rgba(0, 0, 0, 0.1)' :
33747e8ea635SAtari911                      '0 0 5px ' . $themeStyles['shadow'];
33757e8ea635SAtari911        $panelContentBg = ($theme === 'professional') ? 'rgba(255, 255, 255, 0.95)' :
33769ccd446eSAtari911                         ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.5)');
33779ccd446eSAtari911        $panelHeaderShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $panelHeaderBg;
33789ccd446eSAtari911
33797e8ea635SAtari911        // Header text color - dark bg text for dark themes, white for light theme accent headers
33807e8ea635SAtari911        $panelHeaderColor = ($theme === 'matrix' || $theme === 'purple' || $theme === 'pink') ? $themeStyles['bg'] :
33817e8ea635SAtari911                            (($theme === 'wiki') ? $themeStyles['text_primary'] : '#fff');
33829ccd446eSAtari911
33837e8ea635SAtari911        $html .= '<div id="selected-day-events-' . $calId . '" style="display:none; margin:8px 4px; border-left:3px solid ' . $panelBorderColor . ($theme === 'wiki' ? '' : ' !important') . '; box-shadow:' . $panelShadow . ';">';
33847e8ea635SAtari911        if ($theme === 'wiki') {
33859ccd446eSAtari911            $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;">';
33861d05cddcSAtari911            $html .= '<span id="selected-day-title-' . $calId . '"></span>';
33879ccd446eSAtari911            $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>';
33887e8ea635SAtari911        } else {
33897e8ea635SAtari911            $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;">';
33907e8ea635SAtari911            $html .= '<span id="selected-day-title-' . $calId . '"></span>';
33917e8ea635SAtari911            $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>';
33927e8ea635SAtari911        }
33931d05cddcSAtari911        $html .= '</div>';
33949ccd446eSAtari911        $html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:' . $panelContentBg . ';"></div>';
33951d05cddcSAtari911        $html .= '</div>';
33961d05cddcSAtari911
33971d05cddcSAtari911        // Add JavaScript for day selection with event data
33981d05cddcSAtari911        $html .= '<script>';
33991d05cddcSAtari911        // Sanitize calId for JavaScript variable names
34001d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
34011d05cddcSAtari911        $html .= 'window.weekEventsData_' . $jsCalId . ' = ' . json_encode($weekEvents) . ';';
34029ccd446eSAtari911
34039ccd446eSAtari911        // Pass theme colors to JavaScript
34049ccd446eSAtari911        $jsThemeColors = json_encode([
34059ccd446eSAtari911            'text_primary' => $themeStyles['text_primary'],
34069ccd446eSAtari911            'text_bright' => $themeStyles['text_bright'],
34079ccd446eSAtari911            'text_dim' => $themeStyles['text_dim'],
34087e8ea635SAtari911            'text_shadow' => ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $themeStyles['text_primary'] :
34097e8ea635SAtari911                             ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $themeStyles['text_primary'] : ''),
34109ccd446eSAtari911            'event_bg' => $theme === 'professional' ? 'rgba(255, 255, 255, 0.5)' :
34119ccd446eSAtari911                         ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.3)'),
34129ccd446eSAtari911            'border_color' => $theme === 'professional' ? 'rgba(0, 0, 0, 0.1)' :
34139ccd446eSAtari911                             ($theme === 'purple' ? 'rgba(155, 89, 182, 0.2)' :
34149ccd446eSAtari911                             ($theme === 'pink' ? 'rgba(255, 20, 147, 0.3)' :
34159ccd446eSAtari911                             ($theme === 'wiki' ? $themeStyles['grid_border'] : 'rgba(0, 204, 7, 0.2)'))),
34169ccd446eSAtari911            'bar_shadow' => $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' :
34179ccd446eSAtari911                           ($theme === 'wiki' ? '0 1px 2px rgba(0,0,0,0.15)' : '0 0 3px')
34189ccd446eSAtari911        ]);
34199ccd446eSAtari911        $html .= 'window.themeColors_' . $jsCalId . ' = ' . $jsThemeColors . ';';
34201d05cddcSAtari911        $html .= '
34211d05cddcSAtari911        window.showDayEvents_' . $jsCalId . ' = function(dateKey) {
34221d05cddcSAtari911            const eventsData = window.weekEventsData_' . $jsCalId . ';
34231d05cddcSAtari911            const container = document.getElementById("selected-day-events-' . $calId . '");
34241d05cddcSAtari911            const title = document.getElementById("selected-day-title-' . $calId . '");
34251d05cddcSAtari911            const content = document.getElementById("selected-day-content-' . $calId . '");
34261d05cddcSAtari911
34271d05cddcSAtari911            if (!eventsData[dateKey] || eventsData[dateKey].length === 0) return;
34281d05cddcSAtari911
34291d05cddcSAtari911            // Format date for display
34301d05cddcSAtari911            const dateObj = new Date(dateKey + "T00:00:00");
34311d05cddcSAtari911            const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" });
34321d05cddcSAtari911            const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" });
34331d05cddcSAtari911            title.textContent = dayName + ", " + monthDay;
34341d05cddcSAtari911
34351d05cddcSAtari911            // Clear content
34361d05cddcSAtari911            content.innerHTML = "";
34371d05cddcSAtari911
3438231d0edbSAtari911            // Sort events by time (all-day events first, then timed events chronologically)
34391d05cddcSAtari911            const sortedEvents = [...eventsData[dateKey]].sort((a, b) => {
3440231d0edbSAtari911                // All-day events (no time) go to the beginning
34411d05cddcSAtari911                if (!a.time && !b.time) return 0;
3442231d0edbSAtari911                if (!a.time) return -1;  // a is all-day, comes first
3443231d0edbSAtari911                if (!b.time) return 1;   // b is all-day, comes first
34441d05cddcSAtari911
34451d05cddcSAtari911                // Compare times (format: "HH:MM")
34461d05cddcSAtari911                const timeA = a.time.split(":").map(Number);
34471d05cddcSAtari911                const timeB = b.time.split(":").map(Number);
34481d05cddcSAtari911                const minutesA = timeA[0] * 60 + timeA[1];
34491d05cddcSAtari911                const minutesB = timeB[0] * 60 + timeB[1];
34501d05cddcSAtari911
34511d05cddcSAtari911                return minutesA - minutesB;
34521d05cddcSAtari911            });
34531d05cddcSAtari911
34549ccd446eSAtari911            // Build events HTML with single color bar (event color only) - theme-aware
34559ccd446eSAtari911            const themeColors = window.themeColors_' . $jsCalId . ';
34561d05cddcSAtari911            sortedEvents.forEach(event => {
34579ccd446eSAtari911                const eventColor = event.color || themeColors.text_primary;
34581d05cddcSAtari911
34591d05cddcSAtari911                const eventDiv = document.createElement("div");
34609ccd446eSAtari911                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;";
34611d05cddcSAtari911
34621d05cddcSAtari911                let eventHTML = "";
34631d05cddcSAtari911
34649ccd446eSAtari911                // Event assigned color bar (single bar on left) - theme-aware shadow
34659ccd446eSAtari911                const barShadow = themeColors.bar_shadow + (themeColors.bar_shadow.includes("rgba") ? "" : " " + eventColor);
34669ccd446eSAtari911                eventHTML += "<div style=\\"width:3px; align-self:stretch; background:" + eventColor + "; border-radius:1px; flex-shrink:0; box-shadow:" + barShadow + ";\\"></div>";
34671d05cddcSAtari911
3468231d0edbSAtari911                // Content wrapper
3469231d0edbSAtari911                eventHTML += "<div style=\\"flex:1; min-width:0; display:flex; justify-content:space-between; align-items:start; gap:4px;\\">";
34701d05cddcSAtari911
3471231d0edbSAtari911                // Left side: event details
34721d05cddcSAtari911                eventHTML += "<div style=\\"flex:1; min-width:0;\\">";
34739ccd446eSAtari911                eventHTML += "<div style=\\"font-weight:600; color:" + themeColors.text_primary + "; word-wrap:break-word; font-family:system-ui, sans-serif; " + themeColors.text_shadow + ";\\">";
34741d05cddcSAtari911
34751d05cddcSAtari911                // Time
34761d05cddcSAtari911                if (event.time) {
34771d05cddcSAtari911                    const timeParts = event.time.split(":");
34781d05cddcSAtari911                    let hours = parseInt(timeParts[0]);
34791d05cddcSAtari911                    const minutes = timeParts[1];
34801d05cddcSAtari911                    const ampm = hours >= 12 ? "PM" : "AM";
34811d05cddcSAtari911                    hours = hours % 12 || 12;
34829ccd446eSAtari911                    eventHTML += "<span style=\\"color:" + themeColors.text_bright + "; font-weight:500; font-size:9px;\\">" + hours + ":" + minutes + " " + ampm + "</span> ";
34831d05cddcSAtari911                }
34841d05cddcSAtari911
34851d05cddcSAtari911                // Title - use HTML version if available
34861d05cddcSAtari911                const titleHTML = event.title_html || event.title || "Untitled";
34871d05cddcSAtari911                eventHTML += titleHTML;
34881d05cddcSAtari911                eventHTML += "</div>";
34891d05cddcSAtari911
34909ccd446eSAtari911                // Description if present - use HTML version - theme-aware color
34911d05cddcSAtari911                if (event.description_html || event.description) {
34921d05cddcSAtari911                    const descHTML = event.description_html || event.description;
34939ccd446eSAtari911                    eventHTML += "<div style=\\"font-size:9px; color:" + themeColors.text_dim + "; margin-top:2px;\\">" + descHTML + "</div>";
34941d05cddcSAtari911                }
34951d05cddcSAtari911
3496231d0edbSAtari911                eventHTML += "</div>"; // Close event details
3497231d0edbSAtari911
34989ccd446eSAtari911                // Right side: conflict badge with tooltip
3499231d0edbSAtari911                if (event.conflict) {
35009ccd446eSAtari911                    let conflictList = [];
35019ccd446eSAtari911                    if (event.conflictingWith && event.conflictingWith.length > 0) {
35029ccd446eSAtari911                        event.conflictingWith.forEach(conf => {
35039ccd446eSAtari911                            const confTime = conf.time + (conf.end_time ? " - " + conf.end_time : "");
35049ccd446eSAtari911                            conflictList.push(conf.title + " (" + confTime + ")");
35059ccd446eSAtari911                        });
35069ccd446eSAtari911                    }
35079ccd446eSAtari911                    const conflictData = btoa(unescape(encodeURIComponent(JSON.stringify(conflictList))));
35089ccd446eSAtari911                    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>";
3509231d0edbSAtari911                }
3510231d0edbSAtari911
3511231d0edbSAtari911                eventHTML += "</div>"; // Close content wrapper
35121d05cddcSAtari911
35131d05cddcSAtari911                eventDiv.innerHTML = eventHTML;
35141d05cddcSAtari911                content.appendChild(eventDiv);
35151d05cddcSAtari911            });
35161d05cddcSAtari911
35171d05cddcSAtari911            container.style.display = "block";
35181d05cddcSAtari911        };
35191d05cddcSAtari911        ';
35201d05cddcSAtari911        $html .= '</script>';
35211d05cddcSAtari911
35221d05cddcSAtari911        return $html;
35231d05cddcSAtari911    }
35241d05cddcSAtari911
35251d05cddcSAtari911    /**
35261d05cddcSAtari911     * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders
35271d05cddcSAtari911     */
352896df7d3eSAtari911    private function renderSidebarSection($title, $events, $accentColor, $calId, $themeStyles, $theme, $importantNsList = ['important']) {
35291d05cddcSAtari911        // Keep the original accent colors for borders
35301d05cddcSAtari911        $borderColor = $accentColor;
35311d05cddcSAtari911
35321d05cddcSAtari911        // Show date for Important Events section
35331d05cddcSAtari911        $showDate = ($title === 'Important Events');
35341d05cddcSAtari911
35359ccd446eSAtari911        // Sort events differently based on section
35369ccd446eSAtari911        if ($title === 'Important Events') {
35379ccd446eSAtari911            // Important Events: sort by date first, then by time
35389ccd446eSAtari911            usort($events, function($a, $b) {
35399ccd446eSAtari911                $aDate = isset($a['date']) ? $a['date'] : '';
35409ccd446eSAtari911                $bDate = isset($b['date']) ? $b['date'] : '';
35411d05cddcSAtari911
35429ccd446eSAtari911                // Different dates - sort by date
35439ccd446eSAtari911                if ($aDate !== $bDate) {
35449ccd446eSAtari911                    return strcmp($aDate, $bDate);
35459ccd446eSAtari911                }
35469ccd446eSAtari911
35479ccd446eSAtari911                // Same date - sort by time
35489ccd446eSAtari911                $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : '';
35499ccd446eSAtari911                $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : '';
35509ccd446eSAtari911
35519ccd446eSAtari911                // All-day events last within same date
35529ccd446eSAtari911                if (empty($aTime) && !empty($bTime)) return 1;
35539ccd446eSAtari911                if (!empty($aTime) && empty($bTime)) return -1;
35549ccd446eSAtari911                if (empty($aTime) && empty($bTime)) return 0;
35559ccd446eSAtari911
35569ccd446eSAtari911                // Both have times
35579ccd446eSAtari911                $aMinutes = $this->timeToMinutes($aTime);
35589ccd446eSAtari911                $bMinutes = $this->timeToMinutes($bTime);
35599ccd446eSAtari911                return $aMinutes - $bMinutes;
35609ccd446eSAtari911            });
35619ccd446eSAtari911        } else {
35629ccd446eSAtari911            // Today/Tomorrow: sort by time only (all same date)
35639ccd446eSAtari911            usort($events, function($a, $b) {
35649ccd446eSAtari911                $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : '';
35659ccd446eSAtari911                $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : '';
35669ccd446eSAtari911
35679ccd446eSAtari911                // All-day events (no time) come first
35689ccd446eSAtari911                if (empty($aTime) && !empty($bTime)) return -1;
35699ccd446eSAtari911                if (!empty($aTime) && empty($bTime)) return 1;
35709ccd446eSAtari911                if (empty($aTime) && empty($bTime)) return 0;
35719ccd446eSAtari911
35729ccd446eSAtari911                // Both have times - convert to minutes for proper chronological sort
35739ccd446eSAtari911                $aMinutes = $this->timeToMinutes($aTime);
35749ccd446eSAtari911                $bMinutes = $this->timeToMinutes($bTime);
35759ccd446eSAtari911
35769ccd446eSAtari911                return $aMinutes - $bMinutes;
35779ccd446eSAtari911            });
35789ccd446eSAtari911        }
35799ccd446eSAtari911
35809ccd446eSAtari911        // Theme-aware section shadow
35817e8ea635SAtari911        $sectionShadow = ($theme === 'professional' || $theme === 'wiki') ?
35827e8ea635SAtari911                        '0 1px 3px rgba(0, 0, 0, 0.1)' :
35837e8ea635SAtari911                        '0 0 5px ' . $themeStyles['shadow'];
35849ccd446eSAtari911
35857e8ea635SAtari911        if ($theme === 'wiki') {
35867e8ea635SAtari911            // Wiki theme: use a background div for the left bar instead of border-left
35877e8ea635SAtari911            // Dark Reader maps border colors differently from background colors, causing mismatch
35887e8ea635SAtari911            $html = '<div style="display:flex; margin:8px 4px; box-shadow:' . $sectionShadow . '; background:' . $themeStyles['bg'] . ';">';
35897e8ea635SAtari911            $html .= '<div style="width:3px; flex-shrink:0; background:' . $borderColor . ';"></div>';
35907e8ea635SAtari911            $html .= '<div style="flex:1; min-width:0;">';
35917e8ea635SAtari911        } else {
35927e8ea635SAtari911            $html = '<div style="border-left:3px solid ' . $borderColor . ' !important; margin:8px 4px; box-shadow:' . $sectionShadow . ';">';
35937e8ea635SAtari911        }
35949ccd446eSAtari911
35957e8ea635SAtari911        // Section header with accent color background - theme-aware
35969ccd446eSAtari911        $headerShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $accentColor;
35977e8ea635SAtari911        $headerTextColor = ($theme === 'matrix' || $theme === 'purple' || $theme === 'pink') ? $themeStyles['bg'] :
35987e8ea635SAtari911                           (($theme === 'wiki') ? $themeStyles['text_primary'] : '#fff');
35997e8ea635SAtari911        if ($theme === 'wiki') {
36007e8ea635SAtari911            // Wiki theme: no !important — let Dark Reader adjust these
36019ccd446eSAtari911            $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 . ';">';
36027e8ea635SAtari911        } else {
36037e8ea635SAtari911            // Dark themes + professional: lock colors against Dark Reader
36047e8ea635SAtari911            $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 . ';">';
36057e8ea635SAtari911        }
36061d05cddcSAtari911        $html .= htmlspecialchars($title);
36071d05cddcSAtari911        $html .= '</div>';
36081d05cddcSAtari911
36099ccd446eSAtari911        // Events - no background (transparent)
36109ccd446eSAtari911        $html .= '<div style="padding:4px 0;">';
36111d05cddcSAtari911
36121d05cddcSAtari911        foreach ($events as $event) {
361396df7d3eSAtari911            $html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor, $themeStyles, $theme, $importantNsList);
36141d05cddcSAtari911        }
36151d05cddcSAtari911
36161d05cddcSAtari911        $html .= '</div>';
36171d05cddcSAtari911        $html .= '</div>';
36187e8ea635SAtari911        if ($theme === 'wiki') {
36197e8ea635SAtari911            $html .= '</div>'; // Close flex wrapper
36207e8ea635SAtari911        }
36211d05cddcSAtari911
36221d05cddcSAtari911        return $html;
36231d05cddcSAtari911    }
36241d05cddcSAtari911
36251d05cddcSAtari911    /**
36269ccd446eSAtari911     * Render individual event in sidebar - Theme-aware
36271d05cddcSAtari911     */
362896df7d3eSAtari911    private function renderSidebarEvent($event, $calId, $showDate = false, $sectionColor = '#00cc07', $themeStyles = null, $theme = 'matrix', $importantNsList = ['important']) {
36291d05cddcSAtari911        $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
36301d05cddcSAtari911        $time = isset($event['time']) ? $event['time'] : '';
36311d05cddcSAtari911        $endTime = isset($event['endTime']) ? $event['endTime'] : '';
36329ccd446eSAtari911        $eventColor = isset($event['color']) ? htmlspecialchars($event['color']) : ($themeStyles ? $themeStyles['text_primary'] : '#00cc07');
36331d05cddcSAtari911        $date = isset($event['date']) ? $event['date'] : '';
36341d05cddcSAtari911        $isTask = isset($event['isTask']) && $event['isTask'];
36351d05cddcSAtari911        $completed = isset($event['completed']) && $event['completed'];
36361d05cddcSAtari911
363796df7d3eSAtari911        // Check if this is an important namespace event
363896df7d3eSAtari911        $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
363996df7d3eSAtari911        $isImportantNs = false;
364096df7d3eSAtari911        foreach ($importantNsList as $impNs) {
364196df7d3eSAtari911            if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
364296df7d3eSAtari911                $isImportantNs = true;
364396df7d3eSAtari911                break;
364496df7d3eSAtari911            }
364596df7d3eSAtari911        }
364696df7d3eSAtari911
36479ccd446eSAtari911        // Theme-aware colors
36489ccd446eSAtari911        $titleColor = $themeStyles ? $themeStyles['text_primary'] : '#00cc07';
36499ccd446eSAtari911        $timeColor = $themeStyles ? $themeStyles['text_bright'] : '#00dd00';
36507e8ea635SAtari911        $textShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $titleColor . ';' :
36517e8ea635SAtari911                      ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $titleColor . ';' : '');
36521d05cddcSAtari911
36539ccd446eSAtari911        // Check for conflicts (using 'conflict' field set by detectTimeConflicts)
36549ccd446eSAtari911        $hasConflict = isset($event['conflict']) && $event['conflict'];
36559ccd446eSAtari911        $conflictingWith = isset($event['conflictingWith']) ? $event['conflictingWith'] : [];
36569ccd446eSAtari911
36579ccd446eSAtari911        // Build conflict list for tooltip
36589ccd446eSAtari911        $conflictList = [];
36599ccd446eSAtari911        if ($hasConflict && !empty($conflictingWith)) {
36609ccd446eSAtari911            foreach ($conflictingWith as $conf) {
36619ccd446eSAtari911                $confTime = $this->formatTimeDisplay($conf['time'], isset($conf['end_time']) ? $conf['end_time'] : '');
36629ccd446eSAtari911                $conflictList[] = $conf['title'] . ' (' . $confTime . ')';
36639ccd446eSAtari911            }
36649ccd446eSAtari911        }
36659ccd446eSAtari911
366696df7d3eSAtari911        // No background on individual events (transparent) - unless important namespace
36679ccd446eSAtari911        // Use theme grid_border with slight opacity for subtle divider
36689ccd446eSAtari911        $borderColor = $themeStyles['grid_border'];
36699ccd446eSAtari911
367096df7d3eSAtari911        // Important namespace highlighting - subtle themed background
367196df7d3eSAtari911        $importantBg = '';
367296df7d3eSAtari911        $importantBorder = '';
367396df7d3eSAtari911        if ($isImportantNs) {
367496df7d3eSAtari911            // Theme-specific important highlighting
367596df7d3eSAtari911            switch ($theme) {
367696df7d3eSAtari911                case 'matrix':
367796df7d3eSAtari911                    $importantBg = 'background:rgba(0,204,7,0.08);';
367896df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);';
367996df7d3eSAtari911                    break;
368096df7d3eSAtari911                case 'purple':
368196df7d3eSAtari911                    $importantBg = 'background:rgba(156,39,176,0.08);';
368296df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(156,39,176,0.4);';
368396df7d3eSAtari911                    break;
368496df7d3eSAtari911                case 'pink':
368596df7d3eSAtari911                    $importantBg = 'background:rgba(255,105,180,0.1);';
368696df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(255,105,180,0.5);';
368796df7d3eSAtari911                    break;
368896df7d3eSAtari911                case 'professional':
368996df7d3eSAtari911                    $importantBg = 'background:rgba(33,150,243,0.08);';
369096df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(33,150,243,0.4);';
369196df7d3eSAtari911                    break;
369296df7d3eSAtari911                case 'wiki':
369396df7d3eSAtari911                    $importantBg = 'background:rgba(0,102,204,0.06);';
369496df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(0,102,204,0.3);';
369596df7d3eSAtari911                    break;
369696df7d3eSAtari911                default:
369796df7d3eSAtari911                    $importantBg = 'background:rgba(0,204,7,0.08);';
369896df7d3eSAtari911                    $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);';
369996df7d3eSAtari911            }
370096df7d3eSAtari911        }
370196df7d3eSAtari911
370296df7d3eSAtari911        $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 . '">';
37031d05cddcSAtari911
3704231d0edbSAtari911        // Event's assigned color bar (single bar on the left)
37059ccd446eSAtari911        $barShadow = ($theme === 'professional') ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . $eventColor;
37069ccd446eSAtari911        $html .= '<div style="width:3px; align-self:stretch; background:' . $eventColor . '; border-radius:1px; flex-shrink:0; box-shadow:' . $barShadow . ';"></div>';
37071d05cddcSAtari911
37081d05cddcSAtari911        // Content
37091d05cddcSAtari911        $html .= '<div style="flex:1; min-width:0;">';
37101d05cddcSAtari911
37111d05cddcSAtari911        // Time + title
37129ccd446eSAtari911        $html .= '<div style="font-weight:600; color:' . $titleColor . '; word-wrap:break-word; font-family:system-ui, sans-serif; ' . $textShadow . '">';
37131d05cddcSAtari911
37141d05cddcSAtari911        if ($time) {
37151d05cddcSAtari911            $displayTime = $this->formatTimeDisplay($time, $endTime);
37169ccd446eSAtari911            $html .= '<span style="color:' . $timeColor . '; font-weight:500; font-size:9px;">' . htmlspecialchars($displayTime) . '</span> ';
37171d05cddcSAtari911        }
37181d05cddcSAtari911
37191d05cddcSAtari911        // Task checkbox
37201d05cddcSAtari911        if ($isTask) {
37211d05cddcSAtari911            $checkIcon = $completed ? '☑' : '☐';
37229ccd446eSAtari911            $checkColor = $themeStyles ? $themeStyles['text_bright'] : '#00ff00';
37239ccd446eSAtari911            $html .= '<span style="font-size:11px; color:' . $checkColor . ';">' . $checkIcon . '</span> ';
37241d05cddcSAtari911        }
37251d05cddcSAtari911
372696df7d3eSAtari911        // Important indicator icon for important namespace events
372796df7d3eSAtari911        if ($isImportantNs) {
372896df7d3eSAtari911            $html .= '<span style="font-size:9px;" title="Important">⭐</span> ';
372996df7d3eSAtari911        }
373096df7d3eSAtari911
37319ccd446eSAtari911        $html .= $title; // Already HTML-escaped on line 2625
37321d05cddcSAtari911
37339ccd446eSAtari911        // Conflict badge using same system as main calendar
37349ccd446eSAtari911        if ($hasConflict && !empty($conflictList)) {
37359ccd446eSAtari911            $conflictJson = base64_encode(json_encode($conflictList));
37369ccd446eSAtari911            $html .= ' <span class="event-conflict-badge" style="font-size:10px;" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . count($conflictList) . '</span>';
37371d05cddcSAtari911        }
37381d05cddcSAtari911
37391d05cddcSAtari911        $html .= '</div>';
37401d05cddcSAtari911
37411d05cddcSAtari911        // Date display BELOW event name for Important events
37421d05cddcSAtari911        if ($showDate && $date) {
37431d05cddcSAtari911            $dateObj = new DateTime($date);
37441d05cddcSAtari911            $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10"
37459ccd446eSAtari911            $dateColor = $themeStyles ? $themeStyles['text_dim'] : '#00aa00';
37467e8ea635SAtari911            $dateShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $dateColor . ';' :
37477e8ea635SAtari911                          ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $dateColor . ';' : '');
37489ccd446eSAtari911            $html .= '<div style="font-size:8px; color:' . $dateColor . '; font-weight:500; margin-top:2px; ' . $dateShadow . '">' . htmlspecialchars($displayDate) . '</div>';
37491d05cddcSAtari911        }
37501d05cddcSAtari911
37511d05cddcSAtari911        $html .= '</div>';
37521d05cddcSAtari911        $html .= '</div>';
37531d05cddcSAtari911
37541d05cddcSAtari911        return $html;
37551d05cddcSAtari911    }
37561d05cddcSAtari911
37571d05cddcSAtari911    /**
37581d05cddcSAtari911     * Format time display (12-hour format with optional end time)
37591d05cddcSAtari911     */
37601d05cddcSAtari911    private function formatTimeDisplay($startTime, $endTime = '') {
37611d05cddcSAtari911        // Convert start time
37621d05cddcSAtari911        list($hour, $minute) = explode(':', $startTime);
37631d05cddcSAtari911        $hour = (int)$hour;
37641d05cddcSAtari911        $ampm = $hour >= 12 ? 'PM' : 'AM';
37651d05cddcSAtari911        $displayHour = $hour % 12;
37661d05cddcSAtari911        if ($displayHour === 0) $displayHour = 12;
37671d05cddcSAtari911
37681d05cddcSAtari911        $display = $displayHour . ':' . $minute . ' ' . $ampm;
37691d05cddcSAtari911
37701d05cddcSAtari911        // Add end time if provided
37711d05cddcSAtari911        if ($endTime && $endTime !== '') {
37721d05cddcSAtari911            list($endHour, $endMinute) = explode(':', $endTime);
37731d05cddcSAtari911            $endHour = (int)$endHour;
37741d05cddcSAtari911            $endAmpm = $endHour >= 12 ? 'PM' : 'AM';
37751d05cddcSAtari911            $endDisplayHour = $endHour % 12;
37761d05cddcSAtari911            if ($endDisplayHour === 0) $endDisplayHour = 12;
37771d05cddcSAtari911
37781d05cddcSAtari911            $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm;
37791d05cddcSAtari911        }
37801d05cddcSAtari911
37811d05cddcSAtari911        return $display;
37821d05cddcSAtari911    }
37831d05cddcSAtari911
37841d05cddcSAtari911    /**
37859ccd446eSAtari911     * Detect time conflicts among events on the same day
37869ccd446eSAtari911     * Returns events array with 'conflict' flag and 'conflictingWith' array
37879ccd446eSAtari911     */
37889ccd446eSAtari911    private function detectTimeConflicts($dayEvents) {
37899ccd446eSAtari911        if (empty($dayEvents)) {
37909ccd446eSAtari911            return $dayEvents;
37919ccd446eSAtari911        }
37929ccd446eSAtari911
37939ccd446eSAtari911        // If only 1 event, no conflicts possible but still add the flag
37949ccd446eSAtari911        if (count($dayEvents) === 1) {
37959ccd446eSAtari911            return [array_merge($dayEvents[0], ['conflict' => false, 'conflictingWith' => []])];
37969ccd446eSAtari911        }
37979ccd446eSAtari911
37989ccd446eSAtari911        $eventsWithFlags = [];
37999ccd446eSAtari911
38009ccd446eSAtari911        foreach ($dayEvents as $i => $event) {
38019ccd446eSAtari911            $hasConflict = false;
38029ccd446eSAtari911            $conflictingWith = [];
38039ccd446eSAtari911
38049ccd446eSAtari911            // Skip all-day events (no time)
38059ccd446eSAtari911            if (empty($event['time'])) {
38069ccd446eSAtari911                $eventsWithFlags[] = array_merge($event, ['conflict' => false, 'conflictingWith' => []]);
38079ccd446eSAtari911                continue;
38089ccd446eSAtari911            }
38099ccd446eSAtari911
38109ccd446eSAtari911            // Get this event's time range
38119ccd446eSAtari911            $startTime = $event['time'];
38129ccd446eSAtari911            // Check both 'end_time' (snake_case) and 'endTime' (camelCase) for compatibility
38139ccd446eSAtari911            $endTime = '';
38149ccd446eSAtari911            if (isset($event['end_time']) && $event['end_time'] !== '') {
38159ccd446eSAtari911                $endTime = $event['end_time'];
38169ccd446eSAtari911            } elseif (isset($event['endTime']) && $event['endTime'] !== '') {
38179ccd446eSAtari911                $endTime = $event['endTime'];
38189ccd446eSAtari911            } else {
38199ccd446eSAtari911                // If no end time, use start time (zero duration) - matches main calendar logic
38209ccd446eSAtari911                $endTime = $startTime;
38219ccd446eSAtari911            }
38229ccd446eSAtari911
38239ccd446eSAtari911            // Check against all other events
38249ccd446eSAtari911            foreach ($dayEvents as $j => $otherEvent) {
38259ccd446eSAtari911                if ($i === $j) continue; // Skip self
38269ccd446eSAtari911                if (empty($otherEvent['time'])) continue; // Skip all-day events
38279ccd446eSAtari911
38289ccd446eSAtari911                $otherStart = $otherEvent['time'];
38299ccd446eSAtari911                // Check both field name formats
38309ccd446eSAtari911                $otherEnd = '';
38319ccd446eSAtari911                if (isset($otherEvent['end_time']) && $otherEvent['end_time'] !== '') {
38329ccd446eSAtari911                    $otherEnd = $otherEvent['end_time'];
38339ccd446eSAtari911                } elseif (isset($otherEvent['endTime']) && $otherEvent['endTime'] !== '') {
38349ccd446eSAtari911                    $otherEnd = $otherEvent['endTime'];
38359ccd446eSAtari911                } else {
38369ccd446eSAtari911                    $otherEnd = $otherStart;
38379ccd446eSAtari911                }
38389ccd446eSAtari911
38399ccd446eSAtari911                // Check for overlap: convert to minutes and compare
38409ccd446eSAtari911                $start1Min = $this->timeToMinutes($startTime);
38419ccd446eSAtari911                $end1Min = $this->timeToMinutes($endTime);
38429ccd446eSAtari911                $start2Min = $this->timeToMinutes($otherStart);
38439ccd446eSAtari911                $end2Min = $this->timeToMinutes($otherEnd);
38449ccd446eSAtari911
38459ccd446eSAtari911                // Overlap if: start1 < end2 AND start2 < end1
38469ccd446eSAtari911                // Note: Using < (not <=) so events that just touch at boundaries don't conflict
38479ccd446eSAtari911                // e.g., 1:00-2:00 and 2:00-3:00 are NOT in conflict
38489ccd446eSAtari911                if ($start1Min < $end2Min && $start2Min < $end1Min) {
38499ccd446eSAtari911                    $hasConflict = true;
38509ccd446eSAtari911                    $conflictingWith[] = [
38519ccd446eSAtari911                        'title' => isset($otherEvent['title']) ? $otherEvent['title'] : 'Untitled',
38529ccd446eSAtari911                        'time' => $otherStart,
38539ccd446eSAtari911                        'end_time' => $otherEnd
38549ccd446eSAtari911                    ];
38559ccd446eSAtari911                }
38569ccd446eSAtari911            }
38579ccd446eSAtari911
38589ccd446eSAtari911            $eventsWithFlags[] = array_merge($event, [
38599ccd446eSAtari911                'conflict' => $hasConflict,
38609ccd446eSAtari911                'conflictingWith' => $conflictingWith
38619ccd446eSAtari911            ]);
38629ccd446eSAtari911        }
38639ccd446eSAtari911
38649ccd446eSAtari911        return $eventsWithFlags;
38659ccd446eSAtari911    }
38669ccd446eSAtari911
38679ccd446eSAtari911    /**
38689ccd446eSAtari911     * Add hours to a time string
38699ccd446eSAtari911     */
38709ccd446eSAtari911    private function addHoursToTime($time, $hours) {
38719ccd446eSAtari911        $totalMinutes = $this->timeToMinutes($time) + ($hours * 60);
38729ccd446eSAtari911        $h = floor($totalMinutes / 60) % 24;
38739ccd446eSAtari911        $m = $totalMinutes % 60;
38749ccd446eSAtari911        return sprintf('%02d:%02d', $h, $m);
38759ccd446eSAtari911    }
38769ccd446eSAtari911
38779ccd446eSAtari911    /**
38781d05cddcSAtari911     * Render DokuWiki syntax to HTML
38791d05cddcSAtari911     * Converts **bold**, //italic//, [[links]], etc. to HTML
38801d05cddcSAtari911     */
38811d05cddcSAtari911    private function renderDokuWikiToHtml($text) {
38821d05cddcSAtari911        if (empty($text)) return '';
38831d05cddcSAtari911
38841d05cddcSAtari911        // Use DokuWiki's parser to render the text
38851d05cddcSAtari911        $instructions = p_get_instructions($text);
38861d05cddcSAtari911
38871d05cddcSAtari911        // Render instructions to XHTML
38881d05cddcSAtari911        $xhtml = p_render('xhtml', $instructions, $info);
38891d05cddcSAtari911
38901d05cddcSAtari911        // Remove surrounding <p> tags if present (we're rendering inline)
38911d05cddcSAtari911        $xhtml = preg_replace('/^<p>(.*)<\/p>$/s', '$1', trim($xhtml));
38921d05cddcSAtari911
38931d05cddcSAtari911        return $xhtml;
38941d05cddcSAtari911    }
38951d05cddcSAtari911
38961d05cddcSAtari911    // Keep old scanForNamespaces for backward compatibility (not used anymore)
38971d05cddcSAtari911    private function scanForNamespaces($dir, $baseNamespace, &$namespaces) {
38981d05cddcSAtari911        if (!is_dir($dir)) return;
38991d05cddcSAtari911
39001d05cddcSAtari911        $items = scandir($dir);
39011d05cddcSAtari911        foreach ($items as $item) {
39021d05cddcSAtari911            if ($item === '.' || $item === '..' || $item === 'calendar') continue;
39031d05cddcSAtari911
39041d05cddcSAtari911            $path = $dir . $item;
39051d05cddcSAtari911            if (is_dir($path)) {
39061d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
39071d05cddcSAtari911                $namespaces[] = $namespace;
39081d05cddcSAtari911                $this->scanForNamespaces($path . '/', $namespace, $namespaces);
39091d05cddcSAtari911            }
39101d05cddcSAtari911        }
39111d05cddcSAtari911    }
39129ccd446eSAtari911
39139ccd446eSAtari911    /**
39149ccd446eSAtari911     * Get current sidebar theme
39159ccd446eSAtari911     */
39169ccd446eSAtari911    private function getSidebarTheme() {
39179ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_theme.txt';
39189ccd446eSAtari911        if (file_exists($configFile)) {
39199ccd446eSAtari911            $theme = trim(file_get_contents($configFile));
39209ccd446eSAtari911            if (in_array($theme, ['matrix', 'purple', 'professional', 'pink', 'wiki'])) {
39219ccd446eSAtari911                return $theme;
39229ccd446eSAtari911            }
39239ccd446eSAtari911        }
39249ccd446eSAtari911        return 'matrix'; // Default
39259ccd446eSAtari911    }
39269ccd446eSAtari911
39279ccd446eSAtari911    /**
39289ccd446eSAtari911     * Get colors from DokuWiki template's style.ini file
39299ccd446eSAtari911     */
39309ccd446eSAtari911    private function getWikiTemplateColors() {
39319ccd446eSAtari911        global $conf;
39329ccd446eSAtari911
39339ccd446eSAtari911        // Get current template name
39349ccd446eSAtari911        $template = $conf['template'];
39359ccd446eSAtari911
39369ccd446eSAtari911        // Try multiple possible locations for style.ini
39379ccd446eSAtari911        $possiblePaths = [
39389ccd446eSAtari911            DOKU_INC . 'conf/tpl/' . $template . '/style.ini',
39399ccd446eSAtari911            DOKU_INC . 'lib/tpl/' . $template . '/style.ini',
39409ccd446eSAtari911        ];
39419ccd446eSAtari911
39429ccd446eSAtari911        $styleIni = null;
39439ccd446eSAtari911        foreach ($possiblePaths as $path) {
39449ccd446eSAtari911            if (file_exists($path)) {
39459ccd446eSAtari911                $styleIni = parse_ini_file($path, true);
39469ccd446eSAtari911                break;
39479ccd446eSAtari911            }
39489ccd446eSAtari911        }
39499ccd446eSAtari911
39509ccd446eSAtari911        if (!$styleIni) {
39519ccd446eSAtari911            return null; // Fall back to CSS variables
39529ccd446eSAtari911        }
39539ccd446eSAtari911
39549ccd446eSAtari911        // Extract color replacements
39559ccd446eSAtari911        $replacements = isset($styleIni['replacements']) ? $styleIni['replacements'] : [];
39569ccd446eSAtari911
39579ccd446eSAtari911        // Map style.ini colors to our theme structure
39589ccd446eSAtari911        $bgSite = isset($replacements['__background_site__']) ? $replacements['__background_site__'] : '#f5f5f5';
39599ccd446eSAtari911        $background = isset($replacements['__background__']) ? $replacements['__background__'] : '#fff';
39609ccd446eSAtari911        $bgAlt = isset($replacements['__background_alt__']) ? $replacements['__background_alt__'] : '#e8e8e8';
39619ccd446eSAtari911        $bgNeu = isset($replacements['__background_neu__']) ? $replacements['__background_neu__'] : '#eee';
39629ccd446eSAtari911        $text = isset($replacements['__text__']) ? $replacements['__text__'] : '#333';
39639ccd446eSAtari911        $textAlt = isset($replacements['__text_alt__']) ? $replacements['__text_alt__'] : '#999';
39649ccd446eSAtari911        $textNeu = isset($replacements['__text_neu__']) ? $replacements['__text_neu__'] : '#666';
39659ccd446eSAtari911        $border = isset($replacements['__border__']) ? $replacements['__border__'] : '#ccc';
39669ccd446eSAtari911        $link = isset($replacements['__link__']) ? $replacements['__link__'] : '#2b73b7';
39679ccd446eSAtari911        $existing = isset($replacements['__existing__']) ? $replacements['__existing__'] : $link;
39689ccd446eSAtari911
39699ccd446eSAtari911        // Build theme colors from template colors
39709ccd446eSAtari911        // ============================================
39719ccd446eSAtari911        // DokuWiki style.ini → Calendar CSS Variable Mapping
39729ccd446eSAtari911        // ============================================
39739ccd446eSAtari911        //   style.ini key         → CSS variable          → Used for
39749ccd446eSAtari911        //   __background_site__   → --background-site     → Container, panel backgrounds
39759ccd446eSAtari911        //   __background__        → --cell-bg             → Cell/input backgrounds (typically white)
39769ccd446eSAtari911        //   __background_alt__    → --background-alt      → Hover states, header backgrounds
39779ccd446eSAtari911        //                         → --background-header
39789ccd446eSAtari911        //   __background_neu__    → --cell-today-bg       → Today cell highlight
39799ccd446eSAtari911        //   __text__              → --text-primary        → Primary text, labels, titles
39809ccd446eSAtari911        //   __text_neu__          → --text-dim            → Secondary text, dates, descriptions
39819ccd446eSAtari911        //   __text_alt__          → (not mapped)          → Available for future use
39829ccd446eSAtari911        //   __border__            → --border-color        → Grid lines, input borders
39837e8ea635SAtari911        //                         → --border-main         → Accent color: buttons, badges, active elements, section headers
39849ccd446eSAtari911        //                         → --header-border
39857e8ea635SAtari911        //   __link__              → --text-bright         → Links, accent text
39869ccd446eSAtari911        //   __existing__          → (fallback to __link__)→ Available for future use
39879ccd446eSAtari911        //
39889ccd446eSAtari911        // To customize: edit your template's conf/style.ini [replacements]
39899ccd446eSAtari911        return [
39909ccd446eSAtari911            'bg' => $bgSite,
39917e8ea635SAtari911            'border' => $border,         // Accent color from template border
39929ccd446eSAtari911            'shadow' => 'rgba(0, 0, 0, 0.1)',
39939ccd446eSAtari911            'header_bg' => $bgAlt,       // Headers use alt background
39949ccd446eSAtari911            'header_border' => $border,
39959ccd446eSAtari911            'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
39969ccd446eSAtari911            'text_primary' => $text,
39979ccd446eSAtari911            'text_bright' => $link,
39989ccd446eSAtari911            'text_dim' => $textNeu,
39999ccd446eSAtari911            'grid_bg' => $bgSite,
40009ccd446eSAtari911            'grid_border' => $border,
40019ccd446eSAtari911            'cell_bg' => $background,    // Cells use __background__ (white/light)
40029ccd446eSAtari911            'cell_today_bg' => $bgNeu,
40039ccd446eSAtari911            'bar_glow' => '0 1px 2px',
400496df7d3eSAtari911            'pastdue_color' => '#e74c3c',
400596df7d3eSAtari911            'pastdue_bg' => '#ffe6e6',
400696df7d3eSAtari911            'pastdue_bg_strong' => '#ffd9d9',
400796df7d3eSAtari911            'pastdue_bg_light' => '#fff2f2',
400896df7d3eSAtari911            'tomorrow_bg' => '#fff9e6',
400996df7d3eSAtari911            'tomorrow_bg_strong' => '#fff4cc',
401096df7d3eSAtari911            'tomorrow_bg_light' => '#fffbf0',
40119ccd446eSAtari911        ];
40129ccd446eSAtari911    }
40139ccd446eSAtari911
40149ccd446eSAtari911    /**
40159ccd446eSAtari911     * Get theme-specific color styles
40169ccd446eSAtari911     */
40179ccd446eSAtari911    private function getSidebarThemeStyles($theme) {
40189ccd446eSAtari911        // For wiki theme, try to read colors from template's style.ini
40199ccd446eSAtari911        if ($theme === 'wiki') {
40209ccd446eSAtari911            $wikiColors = $this->getWikiTemplateColors();
40219ccd446eSAtari911            if (!empty($wikiColors)) {
40229ccd446eSAtari911                return $wikiColors;
40239ccd446eSAtari911            }
40249ccd446eSAtari911            // Fall through to default wiki colors if reading fails
40259ccd446eSAtari911        }
40269ccd446eSAtari911
40279ccd446eSAtari911        $themes = [
40289ccd446eSAtari911            'matrix' => [
40299ccd446eSAtari911                'bg' => '#242424',
40309ccd446eSAtari911                'border' => '#00cc07',
40319ccd446eSAtari911                'shadow' => 'rgba(0, 204, 7, 0.3)',
40329ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2a2a2a 0%, #242424 100%)',
40339ccd446eSAtari911                'header_border' => '#00cc07',
40349ccd446eSAtari911                'header_shadow' => '0 2px 8px rgba(0, 204, 7, 0.3)',
40359ccd446eSAtari911                'text_primary' => '#00cc07',
40369ccd446eSAtari911                'text_bright' => '#00ff00',
40379ccd446eSAtari911                'text_dim' => '#00aa00',
40389ccd446eSAtari911                'grid_bg' => '#1a3d1a',
40399ccd446eSAtari911                'grid_border' => '#00cc07',
40409ccd446eSAtari911                'cell_bg' => '#242424',
40419ccd446eSAtari911                'cell_today_bg' => '#2a4d2a',
40429ccd446eSAtari911                'bar_glow' => '0 0 3px',
40437e8ea635SAtari911                'pastdue_color' => '#e74c3c',
40447e8ea635SAtari911                'pastdue_bg' => '#3d1a1a',
40457e8ea635SAtari911                'pastdue_bg_strong' => '#4d2020',
40467e8ea635SAtari911                'pastdue_bg_light' => '#2d1515',
40477e8ea635SAtari911                'tomorrow_bg' => '#3d3d1a',
40487e8ea635SAtari911                'tomorrow_bg_strong' => '#4d4d20',
40497e8ea635SAtari911                'tomorrow_bg_light' => '#2d2d15',
40509ccd446eSAtari911            ],
40519ccd446eSAtari911            'purple' => [
40529ccd446eSAtari911                'bg' => '#2a2030',
40539ccd446eSAtari911                'border' => '#9b59b6',
40549ccd446eSAtari911                'shadow' => 'rgba(155, 89, 182, 0.3)',
40559ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2f2438 0%, #2a2030 100%)',
40569ccd446eSAtari911                'header_border' => '#9b59b6',
40579ccd446eSAtari911                'header_shadow' => '0 2px 8px rgba(155, 89, 182, 0.3)',
40589ccd446eSAtari911                'text_primary' => '#b19cd9',
40599ccd446eSAtari911                'text_bright' => '#d4a5ff',
40609ccd446eSAtari911                'text_dim' => '#8e7ab8',
40619ccd446eSAtari911                'grid_bg' => '#3d2b4d',
40629ccd446eSAtari911                'grid_border' => '#9b59b6',
40639ccd446eSAtari911                'cell_bg' => '#2a2030',
40649ccd446eSAtari911                'cell_today_bg' => '#3d2b4d',
40659ccd446eSAtari911                'bar_glow' => '0 0 3px',
40667e8ea635SAtari911                'pastdue_color' => '#e74c3c',
40677e8ea635SAtari911                'pastdue_bg' => '#3d1a2a',
40687e8ea635SAtari911                'pastdue_bg_strong' => '#4d2035',
40697e8ea635SAtari911                'pastdue_bg_light' => '#2d1520',
40707e8ea635SAtari911                'tomorrow_bg' => '#3d3520',
40717e8ea635SAtari911                'tomorrow_bg_strong' => '#4d4028',
40727e8ea635SAtari911                'tomorrow_bg_light' => '#2d2a18',
40739ccd446eSAtari911            ],
40749ccd446eSAtari911            'professional' => [
40759ccd446eSAtari911                'bg' => '#f5f7fa',
40769ccd446eSAtari911                'border' => '#4a90e2',
40779ccd446eSAtari911                'shadow' => 'rgba(74, 144, 226, 0.2)',
40789ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #ffffff 0%, #f5f7fa 100%)',
40799ccd446eSAtari911                'header_border' => '#4a90e2',
40809ccd446eSAtari911                'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
40819ccd446eSAtari911                'text_primary' => '#2c3e50',
40829ccd446eSAtari911                'text_bright' => '#4a90e2',
40839ccd446eSAtari911                'text_dim' => '#7f8c8d',
40849ccd446eSAtari911                'grid_bg' => '#e8ecf1',
40859ccd446eSAtari911                'grid_border' => '#d0d7de',
40869ccd446eSAtari911                'cell_bg' => '#ffffff',
40879ccd446eSAtari911                'cell_today_bg' => '#dce8f7',
40889ccd446eSAtari911                'bar_glow' => '0 1px 2px',
40897e8ea635SAtari911                'pastdue_color' => '#e74c3c',
40907e8ea635SAtari911                'pastdue_bg' => '#ffe6e6',
40917e8ea635SAtari911                'pastdue_bg_strong' => '#ffd9d9',
40927e8ea635SAtari911                'pastdue_bg_light' => '#fff2f2',
40937e8ea635SAtari911                'tomorrow_bg' => '#fff9e6',
40947e8ea635SAtari911                'tomorrow_bg_strong' => '#fff4cc',
40957e8ea635SAtari911                'tomorrow_bg_light' => '#fffbf0',
40969ccd446eSAtari911            ],
40979ccd446eSAtari911            'pink' => [
40989ccd446eSAtari911                'bg' => '#1a0d14',
40999ccd446eSAtari911                'border' => '#ff1493',
41009ccd446eSAtari911                'shadow' => 'rgba(255, 20, 147, 0.4)',
41019ccd446eSAtari911                'header_bg' => 'linear-gradient(180deg, #2d1a24 0%, #1a0d14 100%)',
41029ccd446eSAtari911                'header_border' => '#ff1493',
41039ccd446eSAtari911                'header_shadow' => '0 0 12px rgba(255, 20, 147, 0.6)',
41049ccd446eSAtari911                'text_primary' => '#ff69b4',
41059ccd446eSAtari911                'text_bright' => '#ff1493',
41069ccd446eSAtari911                'text_dim' => '#ff85c1',
41079ccd446eSAtari911                'grid_bg' => '#2d1a24',
41089ccd446eSAtari911                'grid_border' => '#ff1493',
41099ccd446eSAtari911                'cell_bg' => '#1a0d14',
41109ccd446eSAtari911                'cell_today_bg' => '#3d2030',
41119ccd446eSAtari911                'bar_glow' => '0 0 5px',
41127e8ea635SAtari911                'pastdue_color' => '#e74c3c',
41137e8ea635SAtari911                'pastdue_bg' => '#3d1520',
41147e8ea635SAtari911                'pastdue_bg_strong' => '#4d1a28',
41157e8ea635SAtari911                'pastdue_bg_light' => '#2d1018',
41167e8ea635SAtari911                'tomorrow_bg' => '#3d3020',
41177e8ea635SAtari911                'tomorrow_bg_strong' => '#4d3a28',
41187e8ea635SAtari911                'tomorrow_bg_light' => '#2d2518',
41199ccd446eSAtari911            ],
41209ccd446eSAtari911            'wiki' => [
41219ccd446eSAtari911                'bg' => '#f5f5f5',
41227e8ea635SAtari911                'border' => '#ccc',          // Template __border__ color
41239ccd446eSAtari911                'shadow' => 'rgba(0, 0, 0, 0.1)',
41249ccd446eSAtari911                'header_bg' => '#e8e8e8',
41259ccd446eSAtari911                'header_border' => '#ccc',
41269ccd446eSAtari911                'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
41279ccd446eSAtari911                'text_primary' => '#333',
41287e8ea635SAtari911                'text_bright' => '#2b73b7',  // Template __link__ color
41299ccd446eSAtari911                'text_dim' => '#666',
41309ccd446eSAtari911                'grid_bg' => '#f5f5f5',
41319ccd446eSAtari911                'grid_border' => '#ccc',
41329ccd446eSAtari911                'cell_bg' => '#fff',
41339ccd446eSAtari911                'cell_today_bg' => '#eee',
41349ccd446eSAtari911                'bar_glow' => '0 1px 2px',
41357e8ea635SAtari911                'pastdue_color' => '#e74c3c',
41367e8ea635SAtari911                'pastdue_bg' => '#ffe6e6',
41377e8ea635SAtari911                'pastdue_bg_strong' => '#ffd9d9',
41387e8ea635SAtari911                'pastdue_bg_light' => '#fff2f2',
41397e8ea635SAtari911                'tomorrow_bg' => '#fff9e6',
41407e8ea635SAtari911                'tomorrow_bg_strong' => '#fff4cc',
41417e8ea635SAtari911                'tomorrow_bg_light' => '#fffbf0',
41429ccd446eSAtari911            ],
41439ccd446eSAtari911        ];
41449ccd446eSAtari911
41459ccd446eSAtari911        return isset($themes[$theme]) ? $themes[$theme] : $themes['matrix'];
41469ccd446eSAtari911    }
41479ccd446eSAtari911
41489ccd446eSAtari911    /**
41499ccd446eSAtari911     * Get week start day preference
41509ccd446eSAtari911     */
41519ccd446eSAtari911    private function getWeekStartDay() {
41529ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt';
41539ccd446eSAtari911        if (file_exists($configFile)) {
41549ccd446eSAtari911            $start = trim(file_get_contents($configFile));
41559ccd446eSAtari911            if (in_array($start, ['monday', 'sunday'])) {
41569ccd446eSAtari911                return $start;
41579ccd446eSAtari911            }
41589ccd446eSAtari911        }
41599ccd446eSAtari911        return 'sunday'; // Default to Sunday (US/Canada standard)
41609ccd446eSAtari911    }
416196df7d3eSAtari911
416296df7d3eSAtari911    /**
416396df7d3eSAtari911     * Get itinerary collapsed default state
416496df7d3eSAtari911     */
416596df7d3eSAtari911    private function getItineraryCollapsed() {
416696df7d3eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_itinerary_collapsed.txt';
416796df7d3eSAtari911        if (file_exists($configFile)) {
416896df7d3eSAtari911            return trim(file_get_contents($configFile)) === 'yes';
416996df7d3eSAtari911        }
417096df7d3eSAtari911        return false; // Default to expanded
417196df7d3eSAtari911    }
417219378907SAtari911}