xref: /plugin/calendar/syntax.php (revision 22228b0ea77db31a6d52d5b3db63727729c1a5e0)
1<?php
2/**
3 * DokuWiki Plugin calendar (Syntax Component)
4 * Compact design with integrated event list
5 *
6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
7 * @author  DokuWiki Community
8 */
9
10if (!defined('DOKU_INC')) die();
11
12class syntax_plugin_calendar extends DokuWiki_Syntax_Plugin {
13
14    public function getType() {
15        return 'substition';
16    }
17
18    public function getPType() {
19        return 'block';
20    }
21
22    public function getSort() {
23        return 155;
24    }
25
26    public function connectTo($mode) {
27        $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
28        $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
29        $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
30    }
31
32    public function handle($match, $state, $pos, Doku_Handler $handler) {
33        $isEventList = (strpos($match, '{{eventlist') === 0);
34        $isEventPanel = (strpos($match, '{{eventpanel') === 0);
35
36        if ($isEventList) {
37            $match = substr($match, 12, -2);
38        } elseif ($isEventPanel) {
39            $match = substr($match, 13, -2);
40        } else {
41            $match = substr($match, 10, -2);
42        }
43
44        $params = array(
45            'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'),
46            'year' => date('Y'),
47            'month' => date('n'),
48            'namespace' => '',
49            'daterange' => '',
50            'date' => '',
51            'range' => ''
52        );
53
54        if (trim($match)) {
55            $pairs = preg_split('/\s+/', trim($match));
56            foreach ($pairs as $pair) {
57                if (strpos($pair, '=') !== false) {
58                    list($key, $value) = explode('=', $pair, 2);
59                    $params[trim($key)] = trim($value);
60                } else {
61                    // Handle standalone flags like "today"
62                    $params[trim($pair)] = true;
63                }
64            }
65        }
66
67        return $params;
68    }
69
70    public function render($mode, Doku_Renderer $renderer, $data) {
71        if ($mode !== 'xhtml') return false;
72
73        // Disable caching - theme can change via admin without page edit
74        $renderer->nocache();
75
76        if ($data['type'] === 'eventlist') {
77            $html = $this->renderStandaloneEventList($data);
78        } elseif ($data['type'] === 'eventpanel') {
79            $html = $this->renderEventPanelOnly($data);
80        } else {
81            $html = $this->renderCompactCalendar($data);
82        }
83
84        $renderer->doc .= $html;
85        return true;
86    }
87
88    private function renderCompactCalendar($data) {
89        $year = (int)$data['year'];
90        $month = (int)$data['month'];
91        $namespace = $data['namespace'];
92
93        // Get theme - prefer inline theme= parameter, fall back to admin default
94        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();
95        $themeStyles = $this->getSidebarThemeStyles($theme);
96        $themeClass = 'calendar-theme-' . $theme;
97
98        // Determine button text color: professional uses white, others use bg color
99        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
100
101        // Check if multiple namespaces or wildcard specified
102        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
103
104        if ($isMultiNamespace) {
105            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
106        } else {
107            $events = $this->loadEvents($namespace, $year, $month);
108        }
109        $calId = 'cal_' . md5(serialize($data) . microtime());
110
111        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
112
113        $prevMonth = $month - 1;
114        $prevYear = $year;
115        if ($prevMonth < 1) {
116            $prevMonth = 12;
117            $prevYear--;
118        }
119
120        $nextMonth = $month + 1;
121        $nextYear = $year;
122        if ($nextMonth > 12) {
123            $nextMonth = 1;
124            $nextYear++;
125        }
126
127        // Get important namespaces from config for highlighting
128        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
129        $importantNsList = ['important']; // default
130        if (file_exists($configFile)) {
131            $config = include $configFile;
132            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
133                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
134            }
135        }
136
137        // Container - all styling via CSS variables
138        $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)) . '">';
139
140        // Inject CSS variables for this calendar instance - all theming flows from here
141        $html .= '<style>
142        #' . $calId . ' {
143            --background-site: ' . $themeStyles['bg'] . ';
144            --background-alt: ' . $themeStyles['cell_bg'] . ';
145            --background-header: ' . $themeStyles['header_bg'] . ';
146            --text-primary: ' . $themeStyles['text_primary'] . ';
147            --text-dim: ' . $themeStyles['text_dim'] . ';
148            --text-bright: ' . $themeStyles['text_bright'] . ';
149            --border-color: ' . $themeStyles['grid_border'] . ';
150            --border-main: ' . $themeStyles['border'] . ';
151            --cell-bg: ' . $themeStyles['cell_bg'] . ';
152            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
153            --shadow-color: ' . $themeStyles['shadow'] . ';
154            --header-border: ' . $themeStyles['header_border'] . ';
155            --header-shadow: ' . $themeStyles['header_shadow'] . ';
156            --grid-bg: ' . $themeStyles['grid_bg'] . ';
157            --btn-text: ' . $btnTextColor . ';
158            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
159            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
160            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
161            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
162            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
163            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
164            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
165        }
166        #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
167        #event-search-' . $calId . '::-webkit-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
168        #event-search-' . $calId . '::-moz-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
169        #event-search-' . $calId . ':-ms-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
170        </style>';
171
172        // Load calendar JavaScript manually (not through DokuWiki concatenation)
173        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
174
175        // Initialize DOKU_BASE for JavaScript
176        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
177
178        // Embed events data as JSON for JavaScript access
179        $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>';
180
181        // Embed language strings for JavaScript
182        $html .= '<script type="application/json" id="calendar-lang-' . $calId . '">' . json_encode($this->getJsLangStrings()) . '</script>';
183
184        // Left side: Calendar
185        $html .= '<div class="calendar-compact-left">';
186
187        // Header with navigation
188        $html .= '<div class="calendar-compact-header">';
189        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
190        $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="' . $this->getLang('click_to_jump') . '">' . $monthName . '</h3>';
191        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
192        $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">' . $this->getLang('today_btn') . '</button>';
193        $html .= '</div>';
194
195        // Calendar grid - day name headers as a separate div (avoids Firefox th height issues)
196        $html .= '<div class="calendar-day-headers">';
197        $html .= '<span>S</span><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span>';
198        $html .= '</div>';
199        $html .= '<table class="calendar-compact-grid">';
200        $html .= '<tbody>';
201
202        $firstDay = mktime(0, 0, 0, $month, 1, $year);
203        $daysInMonth = date('t', $firstDay);
204        $dayOfWeek = date('w', $firstDay);
205
206        // Build a map of all events with their date ranges for the calendar grid
207        $eventRanges = array();
208        foreach ($events as $dateKey => $dayEvents) {
209            foreach ($dayEvents as $evt) {
210                $eventId = isset($evt['id']) ? $evt['id'] : '';
211                $startDate = $dateKey;
212                $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey;
213
214                // Only process events that touch this month
215                $eventStart = new DateTime($startDate);
216                $eventEnd = new DateTime($endDate);
217                $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month));
218                $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth));
219
220                // Skip if event doesn't overlap with current month
221                if ($eventEnd < $monthStart || $eventStart > $monthEnd) {
222                    continue;
223                }
224
225                // Create entry for each day the event spans
226                $current = clone $eventStart;
227                while ($current <= $eventEnd) {
228                    $currentKey = $current->format('Y-m-d');
229
230                    // Check if this date is in current month
231                    $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey);
232                    if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) {
233                        if (!isset($eventRanges[$currentKey])) {
234                            $eventRanges[$currentKey] = array();
235                        }
236
237                        // Add event with span information
238                        $evt['_span_start'] = $startDate;
239                        $evt['_span_end'] = $endDate;
240                        $evt['_is_first_day'] = ($currentKey === $startDate);
241                        $evt['_is_last_day'] = ($currentKey === $endDate);
242                        $evt['_original_date'] = $dateKey; // Keep track of original date
243
244                        // Check if event continues from previous month or to next month
245                        $evt['_continues_from_prev'] = ($eventStart < $monthStart);
246                        $evt['_continues_to_next'] = ($eventEnd > $monthEnd);
247
248                        $eventRanges[$currentKey][] = $evt;
249                    }
250
251                    $current->modify('+1 day');
252                }
253            }
254        }
255
256        $currentDay = 1;
257        $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7);
258
259        for ($row = 0; $row < $rowCount; $row++) {
260            $html .= '<tr>';
261            for ($col = 0; $col < 7; $col++) {
262                if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) {
263                    $html .= '<td class="cal-empty"></td>';
264                } else {
265                    $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay);
266                    $isToday = ($dateKey === date('Y-m-d'));
267                    $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]);
268
269                    $classes = 'cal-day';
270                    if ($isToday) $classes .= ' cal-today';
271                    if ($hasEvents) $classes .= ' cal-has-events';
272
273                    $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">';
274
275                    $dayNumClass = $isToday ? 'day-num day-num-today' : 'day-num';
276                    $html .= '<span class="' . $dayNumClass . '">' . $currentDay . '</span>';
277
278                    if ($hasEvents) {
279                        // Sort events by time (no time first, then by time)
280                        $sortedEvents = $eventRanges[$dateKey];
281                        usort($sortedEvents, function($a, $b) {
282                            $timeA = isset($a['time']) ? $a['time'] : '';
283                            $timeB = isset($b['time']) ? $b['time'] : '';
284
285                            // Events without time go first
286                            if (empty($timeA) && !empty($timeB)) return -1;
287                            if (!empty($timeA) && empty($timeB)) return 1;
288                            if (empty($timeA) && empty($timeB)) return 0;
289
290                            // Sort by time
291                            return strcmp($timeA, $timeB);
292                        });
293
294                        // Show colored stacked bars for each event
295                        $html .= '<div class="event-indicators">';
296                        foreach ($sortedEvents as $evt) {
297                            $eventId = isset($evt['id']) ? $evt['id'] : '';
298                            $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db';
299                            $eventTime = isset($evt['time']) ? $evt['time'] : '';
300                            $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : $this->getLang('default_event');
301                            $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey;
302                            $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true;
303                            $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true;
304
305                            // Check if this event is from an important namespace
306                            $evtNs = isset($evt['namespace']) ? $evt['namespace'] : '';
307                            if (!$evtNs && isset($evt['_namespace'])) {
308                                $evtNs = $evt['_namespace'];
309                            }
310                            $isImportantEvent = false;
311                            foreach ($importantNsList as $impNs) {
312                                if ($evtNs === $impNs || strpos($evtNs, $impNs . ':') === 0) {
313                                    $isImportantEvent = true;
314                                    break;
315                                }
316                            }
317
318                            $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed';
319
320                            // Add classes for multi-day spanning
321                            if (!$isFirstDay) $barClass .= ' event-bar-continues';
322                            if (!$isLastDay) $barClass .= ' event-bar-continuing';
323                            if ($isImportantEvent) {
324                                $barClass .= ' event-bar-important';
325                                if ($isFirstDay) {
326                                    $barClass .= ' event-bar-has-star';
327                                }
328                            }
329
330                            $titlePrefix = $isImportantEvent ? '⭐ ' : '';
331
332                            $html .= '<span class="event-bar ' . $barClass . '" ';
333                            $html .= 'style="background: ' . $eventColor . ';" ';
334                            $html .= 'title="' . $titlePrefix . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" ';
335                            $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">';
336                            $html .= '</span>';
337                        }
338                        $html .= '</div>';
339                    }
340
341                    $html .= '</td>';
342                    $currentDay++;
343                }
344            }
345            $html .= '</tr>';
346        }
347
348        $html .= '</tbody></table>';
349        $html .= '</div>'; // End calendar-left
350
351        // Right side: Event list
352        $html .= '<div class="calendar-compact-right">';
353        $html .= '<div class="event-list-header">';
354        $html .= '<div class="event-list-header-content">';
355        $html .= '<h4 id="eventlist-title-' . $calId . '">' . $this->getLang('events_header') . '</h4>';
356        if ($namespace) {
357            $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>';
358        }
359        $html .= '</div>';
360
361        // Search bar in header
362        $html .= '<div class="event-search-container-inline">';
363        $html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" placeholder="' . $this->getLang('search_placeholder') . '" oninput="filterEvents(\'' . $calId . '\', this.value)">';
364        $html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
365        $html .= '<button class="event-search-mode-inline" id="search-mode-' . $calId . '" onclick="toggleSearchMode(\'' . $calId . '\', \'' . $namespace . '\')" title="' . $this->getLang('search_this_month') . '">��</button>';
366        $html .= '</div>';
367
368        $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">' . $this->getLang('add_btn') . '</button>';
369        $html .= '</div>';
370
371        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">';
372        $html .= $this->renderEventListContent($events, $calId, $namespace, $themeStyles);
373        $html .= '</div>';
374
375        $html .= '</div>'; // End calendar-right
376
377        // Event dialog
378        $html .= $this->renderEventDialog($calId, $namespace, $theme);
379
380        // Month/Year picker dialog (at container level for proper overlay)
381        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
382
383        $html .= '</div>'; // End container
384
385        return $html;
386    }
387
388    private function renderEventListContent($events, $calId, $namespace, $themeStyles = null) {
389        if (empty($events)) {
390            return '<p class="no-events-msg">No events this month</p>';
391        }
392
393        // Default theme styles if not provided
394        if ($themeStyles === null) {
395            $theme = $this->getSidebarTheme();
396            $themeStyles = $this->getSidebarThemeStyles($theme);
397        } else {
398            $theme = $this->getSidebarTheme();
399        }
400
401        // Get important namespaces from config
402        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
403        $importantNsList = ['important']; // default
404        if (file_exists($configFile)) {
405            $config = include $configFile;
406            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
407                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
408            }
409        }
410
411        // Check for time conflicts
412        $events = $this->checkTimeConflicts($events);
413
414        // Sort by date ascending (chronological order - oldest first)
415        ksort($events);
416
417        // Sort events within each day by time
418        foreach ($events as $dateKey => &$dayEvents) {
419            usort($dayEvents, function($a, $b) {
420                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
421                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
422
423                // All-day events (no time) go to the TOP
424                if ($timeA === null && $timeB !== null) return -1; // A before B
425                if ($timeA !== null && $timeB === null) return 1;  // A after B
426                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
427
428                // Both have times, sort chronologically
429                return strcmp($timeA, $timeB);
430            });
431        }
432        unset($dayEvents); // Break reference
433
434        // Get today's date for comparison
435        $today = date('Y-m-d');
436        $firstFutureEventId = null;
437
438        // Helper function to check if event is past (with 15-minute grace period for timed events)
439        $isEventPast = function($dateKey, $time) use ($today) {
440            // If event is on a past date, it's definitely past
441            if ($dateKey < $today) {
442                return true;
443            }
444
445            // If event is on a future date, it's definitely not past
446            if ($dateKey > $today) {
447                return false;
448            }
449
450            // Event is today - check time with grace period
451            if ($time && $time !== '') {
452                try {
453                    $currentDateTime = new DateTime();
454                    $eventDateTime = new DateTime($dateKey . ' ' . $time);
455
456                    // Add 15-minute grace period
457                    $eventDateTime->modify('+15 minutes');
458
459                    // Event is past if current time > event time + 15 minutes
460                    return $currentDateTime > $eventDateTime;
461                } catch (Exception $e) {
462                    // If time parsing fails, fall back to date-only comparison
463                    return false;
464                }
465            }
466
467            // No time specified for today's event, treat as future
468            return false;
469        };
470
471        // Build HTML for each event - separate past/completed from future
472        $pastHtml = '';
473        $futureHtml = '';
474        $pastCount = 0;
475
476        foreach ($events as $dateKey => $dayEvents) {
477
478            foreach ($dayEvents as $event) {
479                // Track first future/today event for auto-scroll
480                if (!$firstFutureEventId && $dateKey >= $today) {
481                    $firstFutureEventId = isset($event['id']) ? $event['id'] : '';
482                }
483                $eventId = isset($event['id']) ? $event['id'] : '';
484                $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
485                $timeRaw = isset($event['time']) ? $event['time'] : '';
486                $time = htmlspecialchars($timeRaw);
487                $endTime = isset($event['endTime']) ? htmlspecialchars($event['endTime']) : '';
488                $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db';
489                $description = isset($event['description']) ? $event['description'] : '';
490                $isTask = isset($event['isTask']) ? $event['isTask'] : false;
491                $completed = isset($event['completed']) ? $event['completed'] : false;
492                $endDate = isset($event['endDate']) ? $event['endDate'] : '';
493
494                // Use helper function to determine if event is past (with grace period)
495                $isPast = $isEventPast($dateKey, $timeRaw);
496                $isToday = $dateKey === $today;
497
498                // Check if event should be in past section
499                // EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past
500                $isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed;
501                if ($isPastOrCompleted) {
502                    $pastCount++;
503                }
504
505                // Determine if task is past due (past date, is task, not completed)
506                $isPastDue = $isPast && $isTask && !$completed;
507
508                // Process description for wiki syntax, HTML, images, and links
509                $renderedDescription = $this->renderDescription($description, $themeStyles);
510
511                // Convert to 12-hour format and handle time ranges
512                $displayTime = '';
513                if ($time) {
514                    $timeObj = DateTime::createFromFormat('H:i', $time);
515                    if ($timeObj) {
516                        $displayTime = $timeObj->format('g:i A');
517
518                        // Add end time if present and different from start time
519                        if ($endTime && $endTime !== $time) {
520                            $endTimeObj = DateTime::createFromFormat('H:i', $endTime);
521                            if ($endTimeObj) {
522                                $displayTime .= ' - ' . $endTimeObj->format('g:i A');
523                            }
524                        }
525                    } else {
526                        $displayTime = $time;
527                    }
528                }
529
530                // Format date display with day of week
531                // Use originalStartDate if this is a multi-month event continuation
532                $displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey;
533                $dateObj = new DateTime($displayDateKey);
534                $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24"
535
536                // Multi-day indicator
537                $multiDay = '';
538                if ($endDate && $endDate !== $displayDateKey) {
539                    $endObj = new DateTime($endDate);
540                    $multiDay = ' → ' . $endObj->format('D, M j');
541                }
542
543                $completedClass = $completed ? ' event-completed' : '';
544                // Don't grey out past due tasks - they need attention!
545                $pastClass = ($isPast && !$isPastDue) ? ' event-past' : '';
546                $pastDueClass = $isPastDue ? ' event-pastdue' : '';
547                $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : '';
548
549                // Check if this is an important namespace event
550                $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
551                if (!$eventNamespace && isset($event['_namespace'])) {
552                    $eventNamespace = $event['_namespace'];
553                }
554                $isImportantNs = false;
555                foreach ($importantNsList as $impNs) {
556                    if ($eventNamespace === $impNs || strpos($eventNamespace, $impNs . ':') === 0) {
557                        $isImportantNs = true;
558                        break;
559                    }
560                }
561                $importantClass = $isImportantNs ? ' event-important' : '';
562
563                // For all themes: use CSS variables, only keep border-left-color as inline
564                $pastClickHandler = ($isPast && !$isPastDue) ? ' onclick="togglePastEventExpand(this)"' : '';
565                $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 . '>';
566                $eventHtml .= '<div class="event-info">';
567
568                $eventHtml .= '<div class="event-title-row">';
569                // Add star for important namespace events
570                if ($isImportantNs) {
571                    $eventHtml .= '<span class="event-important-star" title="' . $this->getLang('important_tooltip') . '">⭐</span> ';
572                }
573                $eventHtml .= '<span class="event-title-compact">' . $title . '</span>';
574                $eventHtml .= '</div>';
575
576                // For past events, hide meta and description (collapsed)
577                // EXCEPTION: Past due tasks should show their details
578                if (!$isPast || $isPastDue) {
579                    $eventHtml .= '<div class="event-meta-compact">';
580                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
581                    if ($displayTime) {
582                        $eventHtml .= ' • ' . $displayTime;
583                    }
584                    // Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks
585                    if ($isPastDue) {
586                        $eventHtml .= ' <span class="event-pastdue-badge" style="background:' . $themeStyles['pastdue_color'] . ' !important; color:white !important; -webkit-text-fill-color:white !important;">' . 'PAST DUE</span>';
587                    } elseif ($isToday) {
588                        $eventHtml .= ' <span class="event-today-badge" style="background:' . $themeStyles['border'] . ' !important; color:' . $themeStyles['bg'] . ' !important; -webkit-text-fill-color:' . $themeStyles['bg'] . ' !important;">' . 'TODAY</span>';
589                    }
590                    // Add namespace badge - ALWAYS show if event has a namespace
591                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
592                    if (!$eventNamespace && isset($event['_namespace'])) {
593                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility
594                    }
595                    // Show badge if namespace exists and is not empty
596                    if ($eventNamespace && $eventNamespace !== '') {
597                        $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>';
598                    }
599
600                    // Add conflict warning if event has time conflicts
601                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
602                        $conflictList = [];
603                        foreach ($event['conflictsWith'] as $conflict) {
604                            $conflictText = $conflict['title'];
605                            if (!empty($conflict['time'])) {
606                                // Format time range
607                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
608                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
609
610                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
611                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
612                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
613                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
614                                } else {
615                                    $conflictText .= ' (' . $startTimeFormatted . ')';
616                                }
617                            }
618                            $conflictList[] = $conflictText;
619                        }
620                        $conflictCount = count($event['conflictsWith']);
621                        $conflictJson = base64_encode(json_encode($conflictList));
622                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
623                    }
624
625                    $eventHtml .= '</span>';
626                    $eventHtml .= '</div>';
627
628                    if ($description) {
629                        $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>';
630                    }
631                } else {
632                    // Past events: render with display:none for click-to-expand
633                    $eventHtml .= '<div class="event-meta-compact" style="display:none;">';
634                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
635                    if ($displayTime) {
636                        $eventHtml .= ' • ' . $displayTime;
637                    }
638                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
639                    if (!$eventNamespace && isset($event['_namespace'])) {
640                        $eventNamespace = $event['_namespace'];
641                    }
642                    if ($eventNamespace && $eventNamespace !== '') {
643                        $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>';
644                    }
645
646                    // Add conflict warning if event has time conflicts
647                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
648                        $conflictList = [];
649                        foreach ($event['conflictsWith'] as $conflict) {
650                            $conflictText = $conflict['title'];
651                            if (!empty($conflict['time'])) {
652                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
653                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
654
655                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
656                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
657                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
658                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
659                                } else {
660                                    $conflictText .= ' (' . $startTimeFormatted . ')';
661                                }
662                            }
663                            $conflictList[] = $conflictText;
664                        }
665                        $conflictCount = count($event['conflictsWith']);
666                        $conflictJson = base64_encode(json_encode($conflictList));
667                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
668                    }
669
670                    $eventHtml .= '</span>';
671                    $eventHtml .= '</div>';
672
673                    if ($description) {
674                        $eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '</div>';
675                    }
676                }
677
678                $eventHtml .= '</div>'; // event-info
679
680                // Use stored namespace from event, fallback to passed namespace
681                $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace;
682
683                $eventHtml .= '<div class="event-actions-compact">';
684                $eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">��️</button>';
685                $eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>';
686                $eventHtml .= '</div>';
687
688                // Checkbox for tasks - ON THE FAR RIGHT
689                if ($isTask) {
690                    $checked = $completed ? 'checked' : '';
691                    $eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">';
692                }
693
694                $eventHtml .= '</div>';
695
696                // Add to appropriate section
697                if ($isPastOrCompleted) {
698                    $pastHtml .= $eventHtml;
699                } else {
700                    $futureHtml .= $eventHtml;
701                }
702            }
703        }
704
705        // Build final HTML with collapsible past events section
706        $html = '';
707
708        // Add collapsible past events section if any exist
709        if ($pastCount > 0) {
710            $html .= '<div class="past-events-section">';
711            $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">';
712            $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> ';
713            $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>';
714            $html .= '</div>';
715            $html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">';
716            $html .= $pastHtml;
717            $html .= '</div>';
718            $html .= '</div>';
719        }
720
721        // Add future events
722        $html .= $futureHtml;
723
724        return $html;
725    }
726
727    /**
728     * Check for time conflicts between events
729     */
730    private function checkTimeConflicts($events) {
731        // Group events by date
732        $eventsByDate = [];
733        foreach ($events as $date => $dateEvents) {
734            if (!is_array($dateEvents)) continue;
735
736            foreach ($dateEvents as $evt) {
737                if (empty($evt['time'])) continue; // Skip all-day events
738
739                if (!isset($eventsByDate[$date])) {
740                    $eventsByDate[$date] = [];
741                }
742                $eventsByDate[$date][] = $evt;
743            }
744        }
745
746        // Check for overlaps on each date
747        foreach ($eventsByDate as $date => $dateEvents) {
748            for ($i = 0; $i < count($dateEvents); $i++) {
749                for ($j = $i + 1; $j < count($dateEvents); $j++) {
750                    if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) {
751                        // Mark both events as conflicting
752                        $dateEvents[$i]['hasConflict'] = true;
753                        $dateEvents[$j]['hasConflict'] = true;
754
755                        // Store conflict info
756                        if (!isset($dateEvents[$i]['conflictsWith'])) {
757                            $dateEvents[$i]['conflictsWith'] = [];
758                        }
759                        if (!isset($dateEvents[$j]['conflictsWith'])) {
760                            $dateEvents[$j]['conflictsWith'] = [];
761                        }
762
763                        $dateEvents[$i]['conflictsWith'][] = [
764                            'id' => $dateEvents[$j]['id'],
765                            'title' => $dateEvents[$j]['title'],
766                            'time' => $dateEvents[$j]['time'],
767                            'endTime' => isset($dateEvents[$j]['endTime']) ? $dateEvents[$j]['endTime'] : ''
768                        ];
769
770                        $dateEvents[$j]['conflictsWith'][] = [
771                            'id' => $dateEvents[$i]['id'],
772                            'title' => $dateEvents[$i]['title'],
773                            'time' => $dateEvents[$i]['time'],
774                            'endTime' => isset($dateEvents[$i]['endTime']) ? $dateEvents[$i]['endTime'] : ''
775                        ];
776                    }
777                }
778            }
779
780            // Update the events array with conflict information
781            foreach ($events[$date] as &$evt) {
782                foreach ($dateEvents as $checkedEvt) {
783                    if ($evt['id'] === $checkedEvt['id']) {
784                        if (isset($checkedEvt['hasConflict'])) {
785                            $evt['hasConflict'] = $checkedEvt['hasConflict'];
786                        }
787                        if (isset($checkedEvt['conflictsWith'])) {
788                            $evt['conflictsWith'] = $checkedEvt['conflictsWith'];
789                        }
790                        break;
791                    }
792                }
793            }
794        }
795
796        return $events;
797    }
798
799    /**
800     * Check if two events overlap in time
801     */
802    private function eventsOverlap($evt1, $evt2) {
803        if (empty($evt1['time']) || empty($evt2['time'])) {
804            return false; // All-day events don't conflict
805        }
806
807        $start1 = $evt1['time'];
808        $end1 = isset($evt1['endTime']) && !empty($evt1['endTime']) ? $evt1['endTime'] : $evt1['time'];
809
810        $start2 = $evt2['time'];
811        $end2 = isset($evt2['endTime']) && !empty($evt2['endTime']) ? $evt2['endTime'] : $evt2['time'];
812
813        // Convert to minutes for easier comparison
814        $start1Mins = $this->timeToMinutes($start1);
815        $end1Mins = $this->timeToMinutes($end1);
816        $start2Mins = $this->timeToMinutes($start2);
817        $end2Mins = $this->timeToMinutes($end2);
818
819        // Check for overlap: start1 < end2 AND start2 < end1
820        return $start1Mins < $end2Mins && $start2Mins < $end1Mins;
821    }
822
823    /**
824     * Convert HH:MM time to minutes since midnight
825     */
826    private function timeToMinutes($timeStr) {
827        $parts = explode(':', $timeStr);
828        if (count($parts) !== 2) return 0;
829
830        return (int)$parts[0] * 60 + (int)$parts[1];
831    }
832
833    private function renderEventPanelOnly($data) {
834        $year = (int)$data['year'];
835        $month = (int)$data['month'];
836        $namespace = $data['namespace'];
837        $height = isset($data['height']) ? $data['height'] : '400px';
838
839        // Validate height format (must be px, em, rem, vh, or %)
840        if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) {
841            $height = '400px'; // Default fallback
842        }
843
844        // Get theme - prefer inline theme= parameter, fall back to admin default
845        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();        $themeStyles = $this->getSidebarThemeStyles($theme);
846
847        // Check if multiple namespaces or wildcard specified
848        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
849
850        if ($isMultiNamespace) {
851            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
852        } else {
853            $events = $this->loadEvents($namespace, $year, $month);
854        }
855        $calId = 'panel_' . md5(serialize($data) . microtime());
856
857        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
858
859        $prevMonth = $month - 1;
860        $prevYear = $year;
861        if ($prevMonth < 1) {
862            $prevMonth = 12;
863            $prevYear--;
864        }
865
866        $nextMonth = $month + 1;
867        $nextYear = $year;
868        if ($nextMonth > 12) {
869            $nextMonth = 1;
870            $nextYear++;
871        }
872
873        // Determine button text color based on theme
874        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
875
876        // Get important namespaces from config for highlighting
877        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
878        $importantNsList = ['important']; // default
879        if (file_exists($configFile)) {
880            $config = include $configFile;
881            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
882                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
883            }
884        }
885
886        $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)) . '">';
887
888        // Inject CSS variables for this panel instance - same as main calendar
889        $html .= '<style>
890        #' . $calId . ' {
891            --background-site: ' . $themeStyles['bg'] . ';
892            --background-alt: ' . $themeStyles['cell_bg'] . ';
893            --background-header: ' . $themeStyles['header_bg'] . ';
894            --text-primary: ' . $themeStyles['text_primary'] . ';
895            --text-dim: ' . $themeStyles['text_dim'] . ';
896            --text-bright: ' . $themeStyles['text_bright'] . ';
897            --border-color: ' . $themeStyles['grid_border'] . ';
898            --border-main: ' . $themeStyles['border'] . ';
899            --cell-bg: ' . $themeStyles['cell_bg'] . ';
900            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
901            --shadow-color: ' . $themeStyles['shadow'] . ';
902            --header-border: ' . $themeStyles['header_border'] . ';
903            --header-shadow: ' . $themeStyles['header_shadow'] . ';
904            --grid-bg: ' . $themeStyles['grid_bg'] . ';
905            --btn-text: ' . $btnTextColor . ';
906            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
907            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
908            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
909            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
910            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
911            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
912            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
913        }
914        #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
915        </style>';
916
917        // Load calendar JavaScript manually (not through DokuWiki concatenation)
918        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
919
920        // Initialize DOKU_BASE for JavaScript
921        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
922
923        // Embed language strings for JavaScript
924        $html .= '<script type="application/json" id="calendar-lang-' . $calId . '">' . json_encode($this->getJsLangStrings()) . '</script>';
925
926        // Compact two-row header designed for ~500px width
927        $html .= '<div class="panel-header-compact">';
928
929        // Row 1: Navigation and title
930        $html .= '<div class="panel-header-row-1">';
931        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
932
933        // Compact month name (e.g. "Feb 2026" instead of "February 2026 Events")
934        $shortMonthName = date('M Y', mktime(0, 0, 0, $month, 1, $year));
935        $html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $shortMonthName . '</h3>';
936
937        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
938
939        // Namespace badge (if applicable)
940        if ($namespace) {
941            if ($isMultiNamespace) {
942                if (strpos($namespace, '*') !== false) {
943                    $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>';
944                } else {
945                    $namespaceList = array_map('trim', explode(';', $namespace));
946                    $nsCount = count($namespaceList);
947                    $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>';
948                }
949            } else {
950                $isFiltering = ($namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false);
951                if ($isFiltering) {
952                    $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>';
953                } else {
954                    $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>';
955                }
956            }
957        }
958
959        $html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">' . $this->getLang('today_btn') . '</button>';
960        $html .= '</div>';
961
962        // Row 2: Search and add button
963        $html .= '<div class="panel-header-row-2">';
964        $html .= '<div class="panel-search-box">';
965        $html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placeholder="Search this month..." oninput="filterEvents(\'' . $calId . '\', this.value)">';
966        $html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
967        $html .= '<button class="panel-search-mode" id="search-mode-' . $calId . '" onclick="toggleSearchMode(\'' . $calId . '\', \'' . $namespace . '\')" title="Search this month only">��</button>';
968        $html .= '</div>';
969        $html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
970        $html .= '</div>';
971
972        $html .= '</div>';
973
974        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">';
975        $html .= $this->renderEventListContent($events, $calId, $namespace);
976        $html .= '</div>';
977
978        $html .= $this->renderEventDialog($calId, $namespace, $theme);
979
980        // Month/Year picker for event panel
981        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
982
983        $html .= '</div>';
984
985        return $html;
986    }
987
988    private function renderStandaloneEventList($data) {
989        $namespace = $data['namespace'];
990        // If no namespace specified, show all namespaces
991        if (empty($namespace)) {
992            $namespace = '*';
993        }
994        $daterange = $data['daterange'];
995        $date = $data['date'];
996        $range = isset($data['range']) ? strtolower($data['range']) : '';
997        $today = isset($data['today']) ? true : false;
998        $sidebar = isset($data['sidebar']) ? true : false;
999        $showchecked = isset($data['showchecked']) ? true : false; // New parameter
1000        $noheader = isset($data['noheader']) ? true : false; // New parameter to hide header
1001
1002        // Handle "range" parameter - day, week, or month
1003        if ($range === 'day') {
1004            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
1005            $endDate = date('Y-m-d');
1006            $headerText = $this->getLang('range_today');
1007        } elseif ($range === 'week') {
1008            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
1009            $endDateTime = new DateTime();
1010            $endDateTime->modify('+7 days');
1011            $endDate = $endDateTime->format('Y-m-d');
1012            $headerText = $this->getLang('range_this_week');
1013        } elseif ($range === 'month') {
1014            $startDate = date('Y-m-01', strtotime('-1 month')); // Include previous month for past due tasks
1015            $endDate = date('Y-m-t'); // Last of current month
1016            $dt = new DateTime();
1017            $headerText = $dt->format('F Y');
1018        } elseif ($sidebar) {
1019            // NEW: Sidebar widget - load current week's events
1020            $weekStartDay = $this->getWeekStartDay(); // Get saved preference
1021
1022            if ($weekStartDay === 'monday') {
1023                // Monday start
1024                $weekStart = date('Y-m-d', strtotime('monday this week'));
1025                $weekEnd = date('Y-m-d', strtotime('sunday this week'));
1026            } else {
1027                // Sunday start (default - US/Canada standard)
1028                $today = date('w'); // 0 (Sun) to 6 (Sat)
1029                if ($today == 0) {
1030                    // Today is Sunday
1031                    $weekStart = date('Y-m-d');
1032                } else {
1033                    // Monday-Saturday: go back to last Sunday
1034                    $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
1035                }
1036                $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
1037            }
1038
1039            // Load events for the entire week PLUS tomorrow (if tomorrow is outside week)
1040            // PLUS next 2 weeks for Important events
1041            $start = new DateTime($weekStart);
1042            $end = new DateTime($weekEnd);
1043
1044            // Check if we need to extend to include tomorrow
1045            $tomorrowDate = date('Y-m-d', strtotime('+1 day'));
1046            if ($tomorrowDate > $weekEnd) {
1047                // Tomorrow is outside the week, extend end date to include it
1048                $end = new DateTime($tomorrowDate);
1049            }
1050
1051            // Extend 2 weeks into the future for Important events
1052            $twoWeeksOut = date('Y-m-d', strtotime($weekEnd . ' +14 days'));
1053            $end = new DateTime($twoWeeksOut);
1054
1055            $end->modify('+1 day'); // DatePeriod excludes end date
1056            $interval = new DateInterval('P1D');
1057            $period = new DatePeriod($start, $interval, $end);
1058
1059            $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
1060            $allEvents = [];
1061            $loadedMonths = [];
1062
1063            foreach ($period as $dt) {
1064                $year = (int)$dt->format('Y');
1065                $month = (int)$dt->format('n');
1066                $dateKey = $dt->format('Y-m-d');
1067
1068                $monthKey = $year . '-' . $month . '-' . $namespace;
1069
1070                if (!isset($loadedMonths[$monthKey])) {
1071                    if ($isMultiNamespace) {
1072                        $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
1073                    } else {
1074                        $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
1075                    }
1076                }
1077
1078                $monthEvents = $loadedMonths[$monthKey];
1079
1080                if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
1081                    $allEvents[$dateKey] = $monthEvents[$dateKey];
1082                }
1083            }
1084
1085            // Apply time conflict detection
1086            $allEvents = $this->checkTimeConflicts($allEvents);
1087
1088            $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8);
1089
1090            // Render sidebar widget and return immediately
1091            $themeOverride = !empty($data['theme']) ? $data['theme'] : null;
1092            return $this->renderSidebarWidget($allEvents, $namespace, $calId, $themeOverride);
1093        } elseif ($today) {
1094            $startDate = date('Y-m-d');
1095            $endDate = date('Y-m-d');
1096            $headerText = $this->getLang('range_today');
1097        } elseif ($daterange) {
1098            list($startDate, $endDate) = explode(':', $daterange);
1099            $start = new DateTime($startDate);
1100            $end = new DateTime($endDate);
1101            $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y');
1102        } elseif ($date) {
1103            $startDate = $date;
1104            $endDate = $date;
1105            $dt = new DateTime($date);
1106            $headerText = $dt->format('l, F j, Y');
1107        } else {
1108            $startDate = date('Y-m-01');
1109            $endDate = date('Y-m-t');
1110            $dt = new DateTime($startDate);
1111            $headerText = $dt->format('F Y');
1112        }
1113
1114        // Load all events in date range
1115        $allEvents = array();
1116        $start = new DateTime($startDate);
1117        $end = new DateTime($endDate);
1118        $end->modify('+1 day');
1119
1120        $interval = new DateInterval('P1D');
1121        $period = new DatePeriod($start, $interval, $end);
1122
1123        // Check if multiple namespaces or wildcard specified
1124        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
1125
1126        static $loadedMonths = array();
1127
1128        foreach ($period as $dt) {
1129            $year = (int)$dt->format('Y');
1130            $month = (int)$dt->format('n');
1131            $dateKey = $dt->format('Y-m-d');
1132
1133            $monthKey = $year . '-' . $month . '-' . $namespace;
1134
1135            if (!isset($loadedMonths[$monthKey])) {
1136                if ($isMultiNamespace) {
1137                    $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
1138                } else {
1139                    $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
1140                }
1141            }
1142
1143            $monthEvents = $loadedMonths[$monthKey];
1144
1145            if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
1146                $allEvents[$dateKey] = $monthEvents[$dateKey];
1147            }
1148        }
1149
1150        // Sort events by date (already sorted by dateKey), then by time within each day
1151        foreach ($allEvents as $dateKey => &$dayEvents) {
1152            usort($dayEvents, function($a, $b) {
1153                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
1154                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
1155
1156                // All-day events (no time) go to the TOP
1157                if ($timeA === null && $timeB !== null) return -1; // A before B
1158                if ($timeA !== null && $timeB === null) return 1;  // A after B
1159                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
1160
1161                // Both have times, sort chronologically
1162                return strcmp($timeA, $timeB);
1163            });
1164        }
1165        unset($dayEvents); // Break reference
1166
1167        // Simple 2-line display widget
1168        $calId = 'eventlist_' . uniqid();
1169        $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();
1170        $themeStyles = $this->getSidebarThemeStyles($theme);
1171        $isDark = in_array($theme, ['matrix', 'purple', 'pink']);
1172        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
1173
1174        // Theme class for CSS targeting
1175        $themeClass = 'eventlist-theme-' . $theme;
1176
1177        // Container styling - dark themes get border + glow, light themes get subtle border
1178        $containerStyle = 'background:' . $themeStyles['bg'] . ' !important;';
1179        if ($isDark) {
1180            $containerStyle .= ' border:2px solid ' . $themeStyles['border'] . ';';
1181            $containerStyle .= ' border-radius:4px;';
1182            $containerStyle .= ' box-shadow:0 0 10px ' . $themeStyles['shadow'] . ';';
1183        } else {
1184            $containerStyle .= ' border:1px solid ' . $themeStyles['grid_border'] . ';';
1185            $containerStyle .= ' border-radius:4px;';
1186        }
1187
1188        $html = '<div class="eventlist-simple ' . $themeClass . '" id="' . $calId . '" style="' . $containerStyle . '">';
1189
1190        // Inject CSS variables for this eventlist instance
1191        $html .= '<style>
1192        #' . $calId . ' {
1193            --background-site: ' . $themeStyles['bg'] . ';
1194            --background-alt: ' . $themeStyles['cell_bg'] . ';
1195            --text-primary: ' . $themeStyles['text_primary'] . ';
1196            --text-dim: ' . $themeStyles['text_dim'] . ';
1197            --text-bright: ' . $themeStyles['text_bright'] . ';
1198            --border-color: ' . $themeStyles['grid_border'] . ';
1199            --border-main: ' . $themeStyles['border'] . ';
1200            --cell-bg: ' . $themeStyles['cell_bg'] . ';
1201            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
1202            --shadow-color: ' . $themeStyles['shadow'] . ';
1203            --grid-bg: ' . $themeStyles['grid_bg'] . ';
1204            --btn-text: ' . $btnTextColor . ';
1205            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
1206            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
1207            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
1208            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
1209            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
1210            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
1211            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
1212        }
1213        </style>';
1214
1215        // Load calendar JavaScript manually (not through DokuWiki concatenation)
1216        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
1217
1218        // Initialize DOKU_BASE for JavaScript
1219        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
1220
1221        // Add compact header with date and clock for "today" mode (unless noheader is set)
1222        if ($today && !empty($allEvents) && !$noheader) {
1223            $todayDate = new DateTime();
1224            $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026"
1225            $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM"
1226
1227            $html .= '<div class="eventlist-today-header">';
1228            $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>';
1229            $html .= '<div class="eventlist-bottom-info">';
1230            $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '">--°</span></span>';
1231            $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>';
1232            $html .= '</div>';
1233
1234            // Three CPU/Memory bars (all update live)
1235            $html .= '<div class="eventlist-stats-container">';
1236
1237            // 5-minute load average (green, updates every 2 seconds)
1238            $html .= '<div class="eventlist-cpu-bar" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $calId . '(\'green\')" onmouseout="hideTooltip_' . $calId . '(\'green\')">';
1239            $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_bright'] . ' !important;"></div>';
1240            $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
1241            $html .= '</div>';
1242
1243            // Real-time CPU (purple, updates with 5-sec average)
1244            $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $calId . '(\'purple\')" onmouseout="hideTooltip_' . $calId . '(\'purple\')">';
1245            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['border'] . ' !important;"></div>';
1246            $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
1247            $html .= '</div>';
1248
1249            // Real-time Memory (orange, updates)
1250            $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $calId . '(\'orange\')" onmouseout="hideTooltip_' . $calId . '(\'orange\')">';
1251            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_primary'] . ' !important;"></div>';
1252            $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
1253            $html .= '</div>';
1254
1255            $html .= '</div>';
1256            $html .= '</div>';
1257
1258            // Add JavaScript to update clock and weather
1259            $html .= '<script>
1260(function() {
1261    // Update clock every second
1262    function updateClock() {
1263        const now = new Date();
1264        let hours = now.getHours();
1265        const minutes = String(now.getMinutes()).padStart(2, "0");
1266        const seconds = String(now.getSeconds()).padStart(2, "0");
1267        const ampm = hours >= 12 ? "PM" : "AM";
1268        hours = hours % 12 || 12;
1269        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
1270        const clockEl = document.getElementById("clock-' . $calId . '");
1271        if (clockEl) clockEl.textContent = timeStr;
1272    }
1273    setInterval(updateClock, 1000);
1274
1275    // Fetch weather - uses default location, click weather to get local
1276    var userLocationGranted = false;
1277    var userLat = 38.5816;  // Sacramento default
1278    var userLon = -121.4944;
1279
1280    function fetchWeatherData(lat, lon) {
1281        fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_weather=true&temperature_unit=fahrenheit")
1282            .then(response => response.json())
1283            .then(data => {
1284                if (data.current_weather) {
1285                    const temp = Math.round(data.current_weather.temperature);
1286                    const weatherCode = data.current_weather.weathercode;
1287                    const icon = getWeatherIcon(weatherCode);
1288                    const iconEl = document.getElementById("weather-icon-' . $calId . '");
1289                    const tempEl = document.getElementById("weather-temp-' . $calId . '");
1290                    if (iconEl) iconEl.textContent = icon;
1291                    if (tempEl) tempEl.innerHTML = temp + "&deg;";
1292                }
1293            })
1294            .catch(error => {
1295                console.log("Weather fetch error:", error);
1296            });
1297    }
1298
1299    function updateWeather() {
1300        fetchWeatherData(userLat, userLon);
1301    }
1302
1303    // Allow user to click weather to get local weather (requires user gesture)
1304    function requestLocalWeather() {
1305        if (userLocationGranted) return; // Already have permission
1306
1307        if ("geolocation" in navigator) {
1308            navigator.geolocation.getCurrentPosition(function(position) {
1309                userLat = position.coords.latitude;
1310                userLon = position.coords.longitude;
1311                userLocationGranted = true;
1312                fetchWeatherData(userLat, userLon);
1313            }, function(error) {
1314                console.log("Geolocation denied or unavailable, using default location");
1315            });
1316        }
1317    }
1318
1319    // Add click handler to weather widget for local weather
1320    setTimeout(function() {
1321        var weatherEl = document.querySelector("#weather-icon-' . $calId . '");
1322        if (weatherEl) {
1323            weatherEl.style.cursor = "pointer";
1324            weatherEl.title = "Click for local weather";
1325            weatherEl.addEventListener("click", requestLocalWeather);
1326        }
1327    }, 100);
1328
1329    // WMO Weather interpretation codes
1330    function getWeatherIcon(code) {
1331        const icons = {
1332            0: "☀️",   // Clear sky
1333            1: "��️",   // Mainly clear
1334            2: "⛅",   // Partly cloudy
1335            3: "☁️",   // Overcast
1336            45: "��️",  // Fog
1337            48: "��️",  // Depositing rime fog
1338            51: "��️",  // Light drizzle
1339            53: "��️",  // Moderate drizzle
1340            55: "��️",  // Dense drizzle
1341            61: "��️",  // Slight rain
1342            63: "��️",  // Moderate rain
1343            65: "⛈️",  // Heavy rain
1344            71: "��️",  // Slight snow
1345            73: "��️",  // Moderate snow
1346            75: "❄️",  // Heavy snow
1347            77: "��️",  // Snow grains
1348            80: "��️",  // Slight rain showers
1349            81: "��️",  // Moderate rain showers
1350            82: "⛈️",  // Violent rain showers
1351            85: "��️",  // Slight snow showers
1352            86: "❄️",  // Heavy snow showers
1353            95: "⛈️",  // Thunderstorm
1354            96: "⛈️",  // Thunderstorm with slight hail
1355            99: "⛈️"   // Thunderstorm with heavy hail
1356        };
1357        return icons[code] || "��️";
1358    }
1359
1360    // Update weather immediately and every 10 minutes
1361    updateWeather();
1362    setInterval(updateWeather, 600000);
1363
1364    // CPU load history for 4-second rolling average
1365    const cpuHistory = [];
1366    const CPU_HISTORY_SIZE = 2; // 2 samples × 2 seconds = 4 seconds
1367
1368    // Store latest system stats for tooltips
1369    let latestStats = {
1370        load: {"1min": 0, "5min": 0, "15min": 0},
1371        uptime: "",
1372        memory_details: {},
1373        top_processes: []
1374    };
1375
1376    // Tooltip functions
1377    window["showTooltip_' . $calId . '"] = function(color) {
1378        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
1379        if (!tooltip) {
1380            console.log("Tooltip element not found for color:", color);
1381            return;
1382        }
1383
1384
1385        let content = "";
1386
1387        if (color === "green") {
1388            // Green bar: Load averages and uptime
1389            content = "<div class=\"tooltip-title\">CPU Load Average</div>";
1390            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
1391            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
1392            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
1393            if (latestStats.uptime) {
1394                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_bright'] . ';\">Uptime: " + latestStats.uptime + "</div>";
1395            }
1396            tooltip.style.setProperty("border-color", "' . $themeStyles['text_bright'] . '", "important");
1397            tooltip.style.setProperty("color", "' . $themeStyles['text_bright'] . '", "important");
1398            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_bright'] . '", "important");
1399        } else if (color === "purple") {
1400            // Purple bar: Load averages (short-term) and top processes
1401            content = "<div class=\"tooltip-title\">CPU Load (Short-term)</div>";
1402            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
1403            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
1404            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
1405                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['border'] . ';\" class=\"tooltip-title\">Top Processes</div>";
1406                latestStats.top_processes.slice(0, 5).forEach(proc => {
1407                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
1408                });
1409            }
1410            tooltip.style.setProperty("border-color", "' . $themeStyles['border'] . '", "important");
1411            tooltip.style.setProperty("color", "' . $themeStyles['border'] . '", "important");
1412            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['border'] . '", "important");
1413        } else if (color === "orange") {
1414            // Orange bar: Memory details and top processes
1415            content = "<div class=\"tooltip-title\">Memory Usage</div>";
1416            if (latestStats.memory_details && latestStats.memory_details.total) {
1417                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
1418                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
1419                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
1420                if (latestStats.memory_details.cached) {
1421                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
1422                }
1423            } else {
1424                content += "<div>Loading...</div>";
1425            }
1426            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
1427                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_primary'] . ';\" class=\"tooltip-title\">Top Processes</div>";
1428                latestStats.top_processes.slice(0, 5).forEach(proc => {
1429                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
1430                });
1431            }
1432            tooltip.style.setProperty("border-color", "' . $themeStyles['text_primary'] . '", "important");
1433            tooltip.style.setProperty("color", "' . $themeStyles['text_primary'] . '", "important");
1434            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_primary'] . '", "important");
1435        }
1436
1437        tooltip.innerHTML = content;
1438        tooltip.style.setProperty("display", "block");
1439        tooltip.style.setProperty("background", "' . $themeStyles['bg'] . '", "important");
1440
1441        // Position tooltip using fixed positioning above the bar
1442        const bar = tooltip.parentElement;
1443        const barRect = bar.getBoundingClientRect();
1444        const tooltipRect = tooltip.getBoundingClientRect();
1445
1446        // Center horizontally on the bar
1447        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
1448        // Position above the bar with 8px gap
1449        const top = barRect.top - tooltipRect.height - 8;
1450
1451        tooltip.style.left = left + "px";
1452        tooltip.style.top = top + "px";
1453    };
1454
1455    window["hideTooltip_' . $calId . '"] = function(color) {
1456        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
1457        if (tooltip) {
1458            tooltip.style.display = "none";
1459        }
1460    };
1461
1462    // Update CPU and memory bars every 2 seconds
1463    function updateSystemStats() {
1464        // Fetch real system stats from server
1465        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
1466            .then(response => response.json())
1467            .then(data => {
1468
1469                // Store data for tooltips
1470                latestStats = {
1471                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
1472                    uptime: data.uptime || "",
1473                    memory_details: data.memory_details || {},
1474                    top_processes: data.top_processes || []
1475                };
1476
1477
1478                // Update green bar (5-minute average) - updates live now!
1479                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
1480                if (greenBar) {
1481                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
1482                }
1483
1484                // Add current CPU to history for purple bar
1485                cpuHistory.push(data.cpu);
1486                if (cpuHistory.length > CPU_HISTORY_SIZE) {
1487                    cpuHistory.shift(); // Remove oldest
1488                }
1489
1490                // Calculate 5-second average for CPU
1491                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
1492
1493                // Update CPU bar (purple) with 5-second average
1494                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
1495                if (cpuBar) {
1496                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
1497                }
1498
1499                // Update memory bar (orange) with real data
1500                const memBar = document.getElementById("mem-realtime-' . $calId . '");
1501                if (memBar) {
1502                    memBar.style.width = Math.min(100, data.memory) + "%";
1503                }
1504            })
1505            .catch(error => {
1506                console.log("System stats error:", error);
1507                // Fallback to client-side estimates on error
1508                const cpuFallback = Math.random() * 100;
1509                cpuHistory.push(cpuFallback);
1510                if (cpuHistory.length > CPU_HISTORY_SIZE) {
1511                    cpuHistory.shift();
1512                }
1513                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
1514
1515                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
1516                if (greenBar) greenBar.style.width = Math.min(100, cpuFallback) + "%";
1517
1518                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
1519                if (cpuBar) cpuBar.style.width = Math.min(100, cpuAverage) + "%";
1520
1521                let memoryUsage = 0;
1522                if (performance.memory) {
1523                    memoryUsage = (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100;
1524                } else {
1525                    memoryUsage = Math.random() * 100;
1526                }
1527                const memBar = document.getElementById("mem-realtime-' . $calId . '");
1528                if (memBar) memBar.style.width = Math.min(100, memoryUsage) + "%";
1529            });
1530    }
1531
1532    // Update immediately and then every 2 seconds
1533    updateSystemStats();
1534    setInterval(updateSystemStats, 2000);
1535})();
1536</script>';
1537        }
1538
1539        if (empty($allEvents)) {
1540            $html .= '<div class="eventlist-simple-empty">';
1541            $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText);
1542            if ($namespace) {
1543                $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>';
1544            }
1545            $html .= '</div>';
1546            $html .= '<div class="eventlist-simple-body">No events</div>';
1547            $html .= '</div>';
1548        } else {
1549            // Calculate today and tomorrow's dates for highlighting
1550            $todayStr = date('Y-m-d');
1551            $tomorrow = date('Y-m-d', strtotime('+1 day'));
1552
1553            foreach ($allEvents as $dateKey => $dayEvents) {
1554                $dateObj = new DateTime($dateKey);
1555                $displayDate = $dateObj->format('D, M j');
1556
1557                // Check if this date is today or tomorrow or past
1558                // Enable highlighting for sidebar mode AND range modes (day, week, month)
1559                $enableHighlighting = $sidebar || !empty($range);
1560                $isToday = $enableHighlighting && ($dateKey === $todayStr);
1561                $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow);
1562                $isPast = $dateKey < $todayStr;
1563
1564                foreach ($dayEvents as $event) {
1565                    // Check if this is a task and if it's completed
1566                    $isTask = !empty($event['isTask']);
1567                    $completed = !empty($event['completed']);
1568
1569                    // ALWAYS skip completed tasks UNLESS showchecked is explicitly set
1570                    if (!$showchecked && $isTask && $completed) {
1571                        continue;
1572                    }
1573
1574                    // Skip past events that are NOT tasks (only show past due tasks from the past)
1575                    if ($isPast && !$isTask) {
1576                        continue;
1577                    }
1578
1579                    // Determine if task is past due (past date, is task, not completed)
1580                    $isPastDue = $isPast && $isTask && !$completed;
1581
1582                    // Line 1: Header (Title, Time, Date, Namespace)
1583                    $todayClass = $isToday ? ' eventlist-simple-today' : '';
1584                    $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : '';
1585                    $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : '';
1586                    $html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . $pastDueClass . '">';
1587                    $html .= '<div class="eventlist-simple-header">';
1588
1589                    // Title
1590                    $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>';
1591
1592                    // Time (12-hour format)
1593                    if (!empty($event['time'])) {
1594                        $timeParts = explode(':', $event['time']);
1595                        if (count($timeParts) === 2) {
1596                            $hour = (int)$timeParts[0];
1597                            $minute = $timeParts[1];
1598                            $ampm = $hour >= 12 ? 'PM' : 'AM';
1599                            $hour = $hour % 12 ?: 12;
1600                            $displayTime = $hour . ':' . $minute . ' ' . $ampm;
1601                            $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>';
1602                        }
1603                    }
1604
1605                    // Date
1606                    $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>';
1607
1608                    // Badge: PAST DUE, TODAY, or nothing
1609                    if ($isPastDue) {
1610                        $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>';
1611                    } elseif ($isToday) {
1612                        $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>';
1613                    }
1614
1615                    // Namespace badge (show individual event's namespace)
1616                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
1617                    if (!$eventNamespace && isset($event['_namespace'])) {
1618                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading
1619                    }
1620                    if ($eventNamespace) {
1621                        $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</span>';
1622                    }
1623
1624                    $html .= '</div>'; // header
1625
1626                    // Line 2: Body (Description only) - only show if description exists
1627                    if (!empty($event['description'])) {
1628                        $html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) . '</div>';
1629                    }
1630
1631                    $html .= '</div>'; // item
1632                }
1633            }
1634        }
1635
1636        $html .= '</div>'; // eventlist-simple
1637
1638        return $html;
1639    }
1640
1641    private function renderEventDialog($calId, $namespace, $theme = null) {
1642        // Get theme for dialog
1643        if ($theme === null) {
1644            $theme = $this->getSidebarTheme();
1645        }
1646        $themeStyles = $this->getSidebarThemeStyles($theme);
1647
1648        $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">';
1649        $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>';
1650
1651        // Draggable dialog with theme
1652        $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">';
1653
1654        // Header with drag handle and close button
1655        $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">';
1656        $html .= '<h3 id="dialog-title-' . $calId . '">' . $this->getLang('dialog_add_event') . '</h3>';
1657        $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>';
1658        $html .= '</div>';
1659
1660        // Form content
1661        $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">';
1662
1663        // Hidden ID field
1664        $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">';
1665
1666        // 1. TITLE
1667        $html .= '<div class="form-field">';
1668        $html .= '<label class="field-label">' . $this->getLang('field_title') . '</label>';
1669        $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek input-compact" placeholder="' . $this->getLang('field_title_placeholder') . '">';
1670        $html .= '</div>';
1671
1672        // 1.5 NAMESPACE SELECTOR (Searchable with fuzzy matching)
1673        $html .= '<div class="form-field">';
1674        $html .= '<label class="field-label">' . $this->getLang('field_namespace') . '</label>';
1675
1676        // Hidden field to store actual selected namespace
1677        $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">';
1678
1679        // Searchable input
1680        $html .= '<div class="namespace-search-wrapper">';
1681        $html .= '<input type="text" id="event-namespace-search-' . $calId . '" class="input-sleek input-compact namespace-search-input" placeholder="' . $this->getLang('field_namespace_placeholder') . '" autocomplete="off">';
1682        $html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="display:none;"></div>';
1683        $html .= '</div>';
1684
1685        // Store namespaces as JSON for JavaScript
1686        $allNamespaces = $this->getAllNamespaces();
1687        $html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($allNamespaces) . '</script>';
1688
1689        $html .= '</div>';
1690
1691        // 2. DESCRIPTION
1692        $html .= '<div class="form-field">';
1693        $html .= '<label class="field-label">' . $this->getLang('field_description') . '</label>';
1694        $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="2" class="input-sleek textarea-sleek textarea-compact" placeholder="' . $this->getLang('field_description_placeholder') . '"></textarea>';
1695        $html .= '</div>';
1696
1697        // 3. START DATE - END DATE (inline)
1698        $html .= '<div class="form-row-group">';
1699
1700        $html .= '<div class="form-field form-field-half">';
1701        $html .= '<label class="field-label-compact">' . $this->getLang('field_start_date') . '</label>';
1702        $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date input-compact">';
1703        $html .= '</div>';
1704
1705        $html .= '<div class="form-field form-field-half">';
1706        $html .= '<label class="field-label-compact">' . $this->getLang('field_end_date') . '</label>';
1707        $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date input-compact" placeholder="Optional">';
1708        $html .= '</div>';
1709
1710        $html .= '</div>'; // End row
1711
1712        // 4. IS REPEATING CHECKBOX
1713        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
1714        $html .= '<label class="checkbox-label checkbox-label-compact">';
1715        $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">';
1716        $html .= '<span>' . $this->getLang('recurring_label') . '</span>';
1717        $html .= '</label>';
1718        $html .= '</div>';
1719
1720        // Recurring options (shown when checkbox is checked)
1721        $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));">';
1722
1723        // Row 1: Repeat every [N] [period]
1724        $html .= '<div class="form-row-group" style="margin-bottom:6px;">';
1725
1726        $html .= '<div class="form-field" style="flex:0 0 auto; min-width:0;">';
1727        $html .= '<label class="field-label-compact">' . $this->getLang('recurring_repeat_every') . '</label>';
1728        $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;">';
1729        $html .= '</div>';
1730
1731        $html .= '<div class="form-field" style="flex:1; min-width:0;">';
1732        $html .= '<label class="field-label-compact">&nbsp;</label>';
1733        $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek input-compact" onchange="updateRecurrenceOptions(\'' . $calId . '\')">';
1734        $html .= '<option value="daily">' . $this->getLang('recurring_days') . '</option>';
1735        $html .= '<option value="weekly">' . $this->getLang('recurring_weeks') . '</option>';
1736        $html .= '<option value="monthly">' . $this->getLang('recurring_months') . '</option>';
1737        $html .= '<option value="yearly">' . $this->getLang('recurring_years') . '</option>';
1738        $html .= '</select>';
1739        $html .= '</div>';
1740
1741        $html .= '</div>'; // End row 1
1742
1743        // Row 2: Weekly options - day of week checkboxes
1744        $html .= '<div id="weekly-options-' . $calId . '" class="weekly-options" style="display:none; margin-bottom:6px;">';
1745        $html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">' . $this->getLang('recurring_on_these_days') . '</label>';
1746        $html .= '<div style="display:flex; flex-wrap:wrap; gap:2px;">';
1747        $dayNames = [$this->getLang('day_sun'), $this->getLang('day_mon'), $this->getLang('day_tue'), $this->getLang('day_wed'), $this->getLang('day_thu'), $this->getLang('day_fri'), $this->getLang('day_sat')];
1748        foreach ($dayNames as $idx => $day) {
1749            $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;">';
1750            $html .= '<input type="checkbox" name="weekDays[]" value="' . $idx . '" style="margin-right:3px; width:12px; height:12px;">';
1751            $html .= '<span>' . $day . '</span>';
1752            $html .= '</label>';
1753        }
1754        $html .= '</div>';
1755        $html .= '</div>'; // End weekly options
1756
1757        // Row 3: Monthly options - day of month OR ordinal weekday
1758        $html .= '<div id="monthly-options-' . $calId . '" class="monthly-options" style="display:none; margin-bottom:6px;">';
1759        $html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">' . $this->getLang('recurring_repeat_on') . '</label>';
1760
1761        // Radio: Day of month vs Ordinal weekday
1762        $html .= '<div style="margin-bottom:6px;">';
1763        $html .= '<label style="display:inline-flex; align-items:center; margin-right:12px; cursor:pointer; font-size:11px;">';
1764        $html .= '<input type="radio" name="monthlyType" value="dayOfMonth" checked onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">';
1765        $html .= $this->getLang('recurring_day_of_month');
1766        $html .= '</label>';
1767        $html .= '<label style="display:inline-flex; align-items:center; cursor:pointer; font-size:11px;">';
1768        $html .= '<input type="radio" name="monthlyType" value="ordinalWeekday" onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">';
1769        $html .= $this->getLang('recurring_weekday_pattern');
1770        $html .= '</label>';
1771        $html .= '</div>';
1772
1773        // Day of month input (shown by default)
1774        $html .= '<div id="monthly-day-' . $calId . '" style="display:flex; align-items:center; gap:6px;">';
1775        $html .= '<span style="font-size:11px;">' . $this->getLang('recurring_day') . '</span>';
1776        $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;">';
1777        $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">' . $this->getLang('recurring_of_each_month') . '</span>';
1778        $html .= '</div>';
1779
1780        // Ordinal weekday (hidden by default)
1781        $html .= '<div id="monthly-ordinal-' . $calId . '" style="display:none;">';
1782        $html .= '<div style="display:flex; align-items:center; gap:4px; flex-wrap:wrap;">';
1783        $html .= '<select id="event-ordinal-' . $calId . '" name="ordinalWeek" class="input-sleek input-compact" style="width:auto;">';
1784        $html .= '<option value="1">' . $this->getLang('ordinal_first') . '</option>';
1785        $html .= '<option value="2">' . $this->getLang('ordinal_second') . '</option>';
1786        $html .= '<option value="3">' . $this->getLang('ordinal_third') . '</option>';
1787        $html .= '<option value="4">' . $this->getLang('ordinal_fourth') . '</option>';
1788        $html .= '<option value="5">' . $this->getLang('ordinal_fifth') . '</option>';
1789        $html .= '<option value="-1">' . $this->getLang('ordinal_last') . '</option>';
1790        $html .= '</select>';
1791        $html .= '<select id="event-ordinal-day-' . $calId . '" name="ordinalDay" class="input-sleek input-compact" style="width:auto;">';
1792        $html .= '<option value="0">' . $this->getLang('day_sunday') . '</option>';
1793        $html .= '<option value="1">' . $this->getLang('day_monday') . '</option>';
1794        $html .= '<option value="2">' . $this->getLang('day_tuesday') . '</option>';
1795        $html .= '<option value="3">' . $this->getLang('day_wednesday') . '</option>';
1796        $html .= '<option value="4">' . $this->getLang('day_thursday') . '</option>';
1797        $html .= '<option value="5">' . $this->getLang('day_friday') . '</option>';
1798        $html .= '<option value="6">' . $this->getLang('day_saturday') . '</option>';
1799        $html .= '</select>';
1800        $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">' . $this->getLang('recurring_of_each_month') . '</span>';
1801        $html .= '</div>';
1802        $html .= '</div>';
1803
1804        $html .= '</div>'; // End monthly options
1805
1806        // Row 4: End date
1807        $html .= '<div class="form-row-group">';
1808        $html .= '<div class="form-field">';
1809        $html .= '<label class="field-label-compact">' . $this->getLang('recurring_until') . '</label>';
1810        $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date input-compact" placeholder="Optional">';
1811        $html .= '<div style="font-size:9px; color:var(--text-dim, #666); margin-top:2px;">' . $this->getLang('recurring_until_hint') . '</div>';
1812        $html .= '</div>';
1813        $html .= '</div>'; // End row 4
1814
1815        $html .= '</div>'; // End recurring options
1816
1817        // 5. TIME (Start & End) - COLOR (inline)
1818        $html .= '<div class="form-row-group">';
1819
1820        $html .= '<div class="form-field form-field-half">';
1821        $html .= '<label class="field-label-compact">' . $this->getLang('field_start_time') . '</label>';
1822        $html .= '<select id="event-time-' . $calId . '" name="time" class="input-sleek input-compact" onchange="updateEndTimeOptions(\'' . $calId . '\')">';
1823        $html .= '<option value="">' . $this->getLang('time_all_day') . '</option>';
1824
1825        // Generate time options in 15-minute intervals
1826        for ($hour = 0; $hour < 24; $hour++) {
1827            for ($minute = 0; $minute < 60; $minute += 15) {
1828                $timeValue = sprintf('%02d:%02d', $hour, $minute);
1829                $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
1830                $ampm = $hour < 12 ? 'AM' : 'PM';
1831                $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
1832                $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
1833            }
1834        }
1835
1836        $html .= '</select>';
1837        $html .= '</div>';
1838
1839        $html .= '<div class="form-field form-field-half">';
1840        $html .= '<label class="field-label-compact">' . $this->getLang('field_end_time') . '</label>';
1841        $html .= '<select id="event-end-time-' . $calId . '" name="endTime" class="input-sleek input-compact">';
1842        $html .= '<option value="">' . $this->getLang('time_same_as_start') . '</option>';
1843
1844        // Generate time options in 15-minute intervals
1845        for ($hour = 0; $hour < 24; $hour++) {
1846            for ($minute = 0; $minute < 60; $minute += 15) {
1847                $timeValue = sprintf('%02d:%02d', $hour, $minute);
1848                $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
1849                $ampm = $hour < 12 ? 'AM' : 'PM';
1850                $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
1851                $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
1852            }
1853        }
1854
1855        $html .= '</select>';
1856        $html .= '</div>';
1857
1858        $html .= '</div>'; // End row
1859
1860        // Color field (new row)
1861        $html .= '<div class="form-row-group">';
1862
1863        $html .= '<div class="form-field form-field-full">';
1864        $html .= '<label class="field-label-compact">' . $this->getLang('field_color') . '</label>';
1865        $html .= '<div class="color-picker-wrapper">';
1866        $html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact color-select" onchange="updateCustomColorPicker(\'' . $calId . '\')">';
1867        $html .= '<option value="#3498db" style="background:#3498db;color:white">' . $this->getLang('color_blue') . '</option>';
1868        $html .= '<option value="#2ecc71" style="background:#2ecc71;color:white">' . $this->getLang('color_green') . '</option>';
1869        $html .= '<option value="#e74c3c" style="background:#e74c3c;color:white">' . $this->getLang('color_red') . '</option>';
1870        $html .= '<option value="#f39c12" style="background:#f39c12;color:white">' . $this->getLang('color_orange') . '</option>';
1871        $html .= '<option value="#9b59b6" style="background:#9b59b6;color:white">' . $this->getLang('color_purple') . '</option>';
1872        $html .= '<option value="#e91e63" style="background:#e91e63;color:white">' . $this->getLang('color_pink') . '</option>';
1873        $html .= '<option value="#1abc9c" style="background:#1abc9c;color:white">' . $this->getLang('color_teal') . '</option>';
1874        $html .= '<option value="custom">' . $this->getLang('color_custom') . '</option>';
1875        $html .= '</select>';
1876        $html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input color-picker-compact" value="#3498db" onchange="updateColorFromPicker(\'' . $calId . '\')">';
1877        $html .= '</div>';
1878        $html .= '</div>';
1879
1880        $html .= '</div>'; // End row
1881
1882        // Task checkbox
1883        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
1884        $html .= '<label class="checkbox-label checkbox-label-compact">';
1885        $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">';
1886        $html .= '<span>' . $this->getLang('task_label') . '</span>';
1887        $html .= '</label>';
1888        $html .= '</div>';
1889
1890        // Action buttons
1891        $html .= '<div class="dialog-actions-sleek">';
1892        $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">' . $this->getLang('btn_cancel') . '</button>';
1893        $html .= '<button type="submit" class="btn-sleek btn-save-sleek">' . $this->getLang('btn_save') . '</button>';
1894        $html .= '</div>';
1895
1896        $html .= '</form>';
1897        $html .= '</div>';
1898        $html .= '</div>';
1899
1900        return $html;
1901    }
1902
1903    private function renderMonthPicker($calId, $year, $month, $namespace, $theme = 'matrix', $themeStyles = null) {
1904        // Fallback to default theme if not provided
1905        if ($themeStyles === null) {
1906            $themeStyles = $this->getSidebarThemeStyles($theme);
1907        }
1908
1909        $themeClass = 'calendar-theme-' . $theme;
1910
1911        $html = '<div class="month-picker-overlay ' . $themeClass . '" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">';
1912        $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">';
1913        $html .= '<h4>Jump to Month</h4>';
1914
1915        $html .= '<div class="month-picker-selects">';
1916        $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">';
1917        $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
1918        for ($m = 1; $m <= 12; $m++) {
1919            $selected = ($m == $month) ? ' selected' : '';
1920            $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>';
1921        }
1922        $html .= '</select>';
1923
1924        $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">';
1925        $currentYear = (int)date('Y');
1926        for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) {
1927            $selected = ($y == $year) ? ' selected' : '';
1928            $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>';
1929        }
1930        $html .= '</select>';
1931        $html .= '</div>';
1932
1933        $html .= '<div class="month-picker-actions">';
1934        $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>';
1935        $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>';
1936        $html .= '</div>';
1937
1938        $html .= '</div>';
1939        $html .= '</div>';
1940
1941        return $html;
1942    }
1943
1944    private function renderDescription($description, $themeStyles = null) {
1945        if (empty($description)) {
1946            return '';
1947        }
1948
1949        // Get theme for link colors if not provided
1950        if ($themeStyles === null) {
1951            $theme = $this->getSidebarTheme();
1952            $themeStyles = $this->getSidebarThemeStyles($theme);
1953        }
1954
1955        $linkColor = '';
1956        $linkStyle = ' class="cal-link"';
1957
1958        // Token-based parsing to avoid escaping issues
1959        $rendered = $description;
1960        $tokens = array();
1961        $tokenIndex = 0;
1962
1963        // Convert DokuWiki image syntax {{image.jpg}} to tokens
1964        $pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/';
1965        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1966        foreach ($matches as $match) {
1967            $imagePath = trim($match[1]);
1968            $alt = isset($match[2]) ? trim($match[2]) : '';
1969
1970            // Handle external URLs
1971            if (preg_match('/^https?:\/\//', $imagePath)) {
1972                $imageHtml = '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
1973            } else {
1974                // Handle internal DokuWiki images
1975                $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath);
1976                $imageHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
1977            }
1978
1979            $token = "\x00TOKEN" . $tokenIndex . "\x00";
1980            $tokens[$tokenIndex] = $imageHtml;
1981            $tokenIndex++;
1982            $rendered = str_replace($match[0], $token, $rendered);
1983        }
1984
1985        // Convert DokuWiki link syntax [[link|text]] to tokens
1986        $pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/';
1987        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1988        foreach ($matches as $match) {
1989            $link = trim($match[1]);
1990            $text = isset($match[2]) ? trim($match[2]) : $link;
1991
1992            // Handle external URLs
1993            if (preg_match('/^https?:\/\//', $link)) {
1994                $linkHtml = '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
1995            } else {
1996                // Handle internal DokuWiki links with section anchors
1997                $parts = explode('#', $link, 2);
1998                $pagePart = $parts[0];
1999                $sectionPart = isset($parts[1]) ? '#' . $parts[1] : '';
2000
2001                $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart;
2002                $linkHtml = '<a href="' . $wikiUrl . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
2003            }
2004
2005            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2006            $tokens[$tokenIndex] = $linkHtml;
2007            $tokenIndex++;
2008            $rendered = str_replace($match[0], $token, $rendered);
2009        }
2010
2011        // Convert markdown-style links [text](url) to tokens
2012        $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
2013        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2014        foreach ($matches as $match) {
2015            $text = trim($match[1]);
2016            $url = trim($match[2]);
2017
2018            if (preg_match('/^https?:\/\//', $url)) {
2019                $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
2020            } else {
2021                $linkHtml = '<a href="' . htmlspecialchars($url) . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>';
2022            }
2023
2024            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2025            $tokens[$tokenIndex] = $linkHtml;
2026            $tokenIndex++;
2027            $rendered = str_replace($match[0], $token, $rendered);
2028        }
2029
2030        // Convert plain URLs to tokens
2031        $pattern = '/(https?:\/\/[^\s<]+)/';
2032        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
2033        foreach ($matches as $match) {
2034            $url = $match[1];
2035            $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($url) . '</a>';
2036
2037            $token = "\x00TOKEN" . $tokenIndex . "\x00";
2038            $tokens[$tokenIndex] = $linkHtml;
2039            $tokenIndex++;
2040            $rendered = str_replace($match[0], $token, $rendered);
2041        }
2042
2043        // NOW escape HTML (tokens are protected)
2044        $rendered = htmlspecialchars($rendered);
2045
2046        // Convert newlines to <br>
2047        $rendered = nl2br($rendered);
2048
2049        // DokuWiki text formatting
2050        // Bold: **text** or __text__
2051        $boldStyle = '';
2052        $rendered = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $rendered);
2053        $rendered = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $rendered);
2054
2055        // Italic: //text//
2056        $rendered = preg_replace('/\/\/(.+?)\/\//', '<em>$1</em>', $rendered);
2057
2058        // Strikethrough: <del>text</del>
2059        $rendered = preg_replace('/&lt;del&gt;(.+?)&lt;\/del&gt;/', '<del>$1</del>', $rendered);
2060
2061        // Monospace: ''text''
2062        $rendered = preg_replace('/&#039;&#039;(.+?)&#039;&#039;/', '<code>$1</code>', $rendered);
2063
2064        // Subscript: <sub>text</sub>
2065        $rendered = preg_replace('/&lt;sub&gt;(.+?)&lt;\/sub&gt;/', '<sub>$1</sub>', $rendered);
2066
2067        // Superscript: <sup>text</sup>
2068        $rendered = preg_replace('/&lt;sup&gt;(.+?)&lt;\/sup&gt;/', '<sup>$1</sup>', $rendered);
2069
2070        // Restore tokens
2071        foreach ($tokens as $i => $html) {
2072            $rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered);
2073        }
2074
2075        return $rendered;
2076    }
2077
2078    private function loadEvents($namespace, $year, $month) {
2079        $dataDir = DOKU_INC . 'data/meta/';
2080        if ($namespace) {
2081            $dataDir .= str_replace(':', '/', $namespace) . '/';
2082        }
2083        $dataDir .= 'calendar/';
2084
2085        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
2086
2087        if (file_exists($eventFile)) {
2088            $json = file_get_contents($eventFile);
2089            return json_decode($json, true);
2090        }
2091
2092        return array();
2093    }
2094
2095    private function loadEventsMultiNamespace($namespaces, $year, $month) {
2096        // Check for wildcard pattern (namespace:*)
2097        if (preg_match('/^(.+):\*$/', $namespaces, $matches)) {
2098            $baseNamespace = $matches[1];
2099            return $this->loadEventsWildcard($baseNamespace, $year, $month);
2100        }
2101
2102        // Check for root wildcard (just *)
2103        if ($namespaces === '*') {
2104            return $this->loadEventsWildcard('', $year, $month);
2105        }
2106
2107        // Parse namespace list (semicolon separated)
2108        // e.g., "team:projects;personal;work:tasks" = three namespaces
2109        $namespaceList = array_map('trim', explode(';', $namespaces));
2110
2111        // Load events from all namespaces
2112        $allEvents = array();
2113        foreach ($namespaceList as $ns) {
2114            $ns = trim($ns);
2115            if (empty($ns)) continue;
2116
2117            $events = $this->loadEvents($ns, $year, $month);
2118
2119            // Add namespace tag to each event
2120            foreach ($events as $dateKey => $dayEvents) {
2121                if (!isset($allEvents[$dateKey])) {
2122                    $allEvents[$dateKey] = array();
2123                }
2124                foreach ($dayEvents as $event) {
2125                    $event['_namespace'] = $ns;
2126                    $allEvents[$dateKey][] = $event;
2127                }
2128            }
2129        }
2130
2131        return $allEvents;
2132    }
2133
2134    private function loadEventsWildcard($baseNamespace, $year, $month) {
2135        // Find all subdirectories under the base namespace
2136        $dataDir = DOKU_INC . 'data/meta/';
2137        if ($baseNamespace) {
2138            $dataDir .= str_replace(':', '/', $baseNamespace) . '/';
2139        }
2140
2141        $allEvents = array();
2142
2143        // First, load events from the base namespace itself
2144        if (empty($baseNamespace)) {
2145            // Root wildcard - load from root calendar
2146            $events = $this->loadEvents('', $year, $month);
2147            foreach ($events as $dateKey => $dayEvents) {
2148                if (!isset($allEvents[$dateKey])) {
2149                    $allEvents[$dateKey] = array();
2150                }
2151                foreach ($dayEvents as $event) {
2152                    $event['_namespace'] = '';
2153                    $allEvents[$dateKey][] = $event;
2154                }
2155            }
2156        } else {
2157            $events = $this->loadEvents($baseNamespace, $year, $month);
2158            foreach ($events as $dateKey => $dayEvents) {
2159                if (!isset($allEvents[$dateKey])) {
2160                    $allEvents[$dateKey] = array();
2161                }
2162                foreach ($dayEvents as $event) {
2163                    $event['_namespace'] = $baseNamespace;
2164                    $allEvents[$dateKey][] = $event;
2165                }
2166            }
2167        }
2168
2169        // Recursively find all subdirectories
2170        $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents);
2171
2172        return $allEvents;
2173    }
2174
2175    private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) {
2176        if (!is_dir($dir)) return;
2177
2178        $items = scandir($dir);
2179        foreach ($items as $item) {
2180            if ($item === '.' || $item === '..') continue;
2181
2182            $path = $dir . $item;
2183            if (is_dir($path) && $item !== 'calendar') {
2184                // This is a namespace directory
2185                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
2186
2187                // Load events from this namespace
2188                $events = $this->loadEvents($namespace, $year, $month);
2189                foreach ($events as $dateKey => $dayEvents) {
2190                    if (!isset($allEvents[$dateKey])) {
2191                        $allEvents[$dateKey] = array();
2192                    }
2193                    foreach ($dayEvents as $event) {
2194                        $event['_namespace'] = $namespace;
2195                        $allEvents[$dateKey][] = $event;
2196                    }
2197                }
2198
2199                // Recurse into subdirectories
2200                $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents);
2201            }
2202        }
2203    }
2204
2205    private function getAllNamespaces() {
2206        $dataDir = DOKU_INC . 'data/meta/';
2207        $namespaces = [];
2208
2209        // Scan for namespaces that have calendar data
2210        $this->scanForCalendarNamespaces($dataDir, '', $namespaces);
2211
2212        // Sort alphabetically
2213        sort($namespaces);
2214
2215        return $namespaces;
2216    }
2217
2218    private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) {
2219        if (!is_dir($dir)) return;
2220
2221        $items = scandir($dir);
2222        foreach ($items as $item) {
2223            if ($item === '.' || $item === '..') continue;
2224
2225            $path = $dir . $item;
2226            if (is_dir($path)) {
2227                // Check if this directory has a calendar subdirectory with data
2228                $calendarDir = $path . '/calendar/';
2229                if (is_dir($calendarDir)) {
2230                    // Check if there are any JSON files in the calendar directory
2231                    $jsonFiles = glob($calendarDir . '*.json');
2232                    if (!empty($jsonFiles)) {
2233                        // This namespace has calendar data
2234                        $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
2235                        $namespaces[] = $namespace;
2236                    }
2237                }
2238
2239                // Recurse into subdirectories
2240                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
2241                $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces);
2242            }
2243        }
2244    }
2245
2246    /**
2247     * Render new sidebar widget - Week at a glance itinerary (200px wide)
2248     */
2249    private function renderSidebarWidget($events, $namespace, $calId, $themeOverride = null) {
2250        if (empty($events)) {
2251            return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">' . $this->getLang('no_events_week') . '</div>';
2252        }
2253
2254        // Get important namespaces from config
2255        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
2256        $importantNsList = ['important']; // default
2257        if (file_exists($configFile)) {
2258            $config = include $configFile;
2259            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
2260                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
2261            }
2262        }
2263
2264        // Calculate date ranges
2265        $todayStr = date('Y-m-d');
2266        $tomorrowStr = date('Y-m-d', strtotime('+1 day'));
2267
2268        // Get week start preference and calculate week range
2269        $weekStartDay = $this->getWeekStartDay();
2270
2271        if ($weekStartDay === 'monday') {
2272            // Monday start
2273            $weekStart = date('Y-m-d', strtotime('monday this week'));
2274            $weekEnd = date('Y-m-d', strtotime('sunday this week'));
2275        } else {
2276            // Sunday start (default - US/Canada standard)
2277            $today = date('w'); // 0 (Sun) to 6 (Sat)
2278            if ($today == 0) {
2279                // Today is Sunday
2280                $weekStart = date('Y-m-d');
2281            } else {
2282                // Monday-Saturday: go back to last Sunday
2283                $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
2284            }
2285            $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
2286        }
2287
2288        // Group events by category
2289        $todayEvents = [];
2290        $tomorrowEvents = [];
2291        $importantEvents = [];
2292        $weekEvents = []; // For week grid
2293
2294        // Process all events
2295        foreach ($events as $dateKey => $dayEvents) {
2296            // Detect conflicts for events on this day
2297            $eventsWithConflicts = $this->detectTimeConflicts($dayEvents);
2298
2299            foreach ($eventsWithConflicts as $event) {
2300                // Always categorize Today and Tomorrow regardless of week boundaries
2301                if ($dateKey === $todayStr) {
2302                    $todayEvents[] = array_merge($event, ['date' => $dateKey]);
2303                }
2304                if ($dateKey === $tomorrowStr) {
2305                    $tomorrowEvents[] = array_merge($event, ['date' => $dateKey]);
2306                }
2307
2308                // Process week grid events (only for current week)
2309                if ($dateKey >= $weekStart && $dateKey <= $weekEnd) {
2310                    // Initialize week grid day if not exists
2311                    if (!isset($weekEvents[$dateKey])) {
2312                        $weekEvents[$dateKey] = [];
2313                    }
2314
2315                    // Pre-render DokuWiki syntax to HTML for JavaScript display
2316                    $eventWithHtml = $event;
2317                    if (isset($event['title'])) {
2318                        $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']);
2319                    }
2320                    if (isset($event['description'])) {
2321                        $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']);
2322                    }
2323                    $weekEvents[$dateKey][] = $eventWithHtml;
2324                }
2325
2326                // Check if this is an important namespace
2327                $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
2328                $isImportant = false;
2329                foreach ($importantNsList as $impNs) {
2330                    if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
2331                        $isImportant = true;
2332                        break;
2333                    }
2334                }
2335
2336                // Important events: show from today through next 2 weeks
2337                if ($isImportant && $dateKey >= $todayStr) {
2338                    $importantEvents[] = array_merge($event, ['date' => $dateKey]);
2339                }
2340            }
2341        }
2342
2343        // Sort Important Events by date (earliest first)
2344        usort($importantEvents, function($a, $b) {
2345            $dateA = isset($a['date']) ? $a['date'] : '';
2346            $dateB = isset($b['date']) ? $b['date'] : '';
2347
2348            // Compare dates
2349            if ($dateA === $dateB) {
2350                // Same date - sort by time
2351                $timeA = isset($a['time']) ? $a['time'] : '';
2352                $timeB = isset($b['time']) ? $b['time'] : '';
2353
2354                if (empty($timeA) && !empty($timeB)) return 1;  // All-day events last
2355                if (!empty($timeA) && empty($timeB)) return -1;
2356                if (empty($timeA) && empty($timeB)) return 0;
2357
2358                // Both have times
2359                $aMinutes = $this->timeToMinutes($timeA);
2360                $bMinutes = $this->timeToMinutes($timeB);
2361                return $aMinutes - $bMinutes;
2362            }
2363
2364            return strcmp($dateA, $dateB);
2365        });
2366
2367        // Get theme - prefer override from syntax parameter, fall back to admin default
2368        $theme = !empty($themeOverride) ? $themeOverride : $this->getSidebarTheme();
2369        $themeStyles = $this->getSidebarThemeStyles($theme);
2370        $themeClass = 'sidebar-' . $theme;
2371
2372        // Start building HTML - Dynamic width with default font (overflow:visible for tooltips)
2373        $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;">';
2374
2375        // Inject CSS variables so the event dialog (shared component) picks up the theme
2376        $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg'];
2377        $html .= '<style>
2378        #sidebar-widget-' . $calId . ' {
2379            --background-site: ' . $themeStyles['bg'] . ';
2380            --background-alt: ' . $themeStyles['cell_bg'] . ';
2381            --background-header: ' . $themeStyles['header_bg'] . ';
2382            --text-primary: ' . $themeStyles['text_primary'] . ';
2383            --text-dim: ' . $themeStyles['text_dim'] . ';
2384            --text-bright: ' . $themeStyles['text_bright'] . ';
2385            --border-color: ' . $themeStyles['grid_border'] . ';
2386            --border-main: ' . $themeStyles['border'] . ';
2387            --cell-bg: ' . $themeStyles['cell_bg'] . ';
2388            --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
2389            --shadow-color: ' . $themeStyles['shadow'] . ';
2390            --header-border: ' . $themeStyles['header_border'] . ';
2391            --header-shadow: ' . $themeStyles['header_shadow'] . ';
2392            --grid-bg: ' . $themeStyles['grid_bg'] . ';
2393            --btn-text: ' . $btnTextColor . ';
2394            --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
2395            --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
2396            --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
2397            --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
2398            --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
2399            --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
2400            --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
2401        }
2402        </style>';
2403
2404        // Embed language strings for JavaScript
2405        $html .= '<script type="application/json" id="calendar-lang-' . $calId . '">' . json_encode($this->getJsLangStrings()) . '</script>';
2406
2407        // Add sparkle effect for pink theme
2408        if ($theme === 'pink') {
2409            $html .= '<style>
2410            @keyframes sparkle-' . $calId . ' {
2411                0% {
2412                    opacity: 0;
2413                    transform: translate(0, 0) scale(0) rotate(0deg);
2414                }
2415                50% {
2416                    opacity: 1;
2417                    transform: translate(var(--tx), var(--ty)) scale(1) rotate(180deg);
2418                }
2419                100% {
2420                    opacity: 0;
2421                    transform: translate(calc(var(--tx) * 2), calc(var(--ty) * 2)) scale(0) rotate(360deg);
2422                }
2423            }
2424
2425            @keyframes pulse-glow-' . $calId . ' {
2426                0%, 100% { box-shadow: 0 0 10px rgba(255, 20, 147, 0.4); }
2427                50% { box-shadow: 0 0 25px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4); }
2428            }
2429
2430            @keyframes shimmer-' . $calId . ' {
2431                0% { background-position: -200% center; }
2432                100% { background-position: 200% center; }
2433            }
2434
2435            .sidebar-pink {
2436                animation: pulse-glow-' . $calId . ' 3s ease-in-out infinite;
2437            }
2438
2439            .sidebar-pink:hover {
2440                box-shadow: 0 0 30px rgba(255, 20, 147, 0.9), 0 0 50px rgba(255, 20, 147, 0.5) !important;
2441            }
2442
2443            .sparkle-' . $calId . ' {
2444                position: absolute;
2445                pointer-events: none;
2446                font-size: 20px;
2447                z-index: 1000;
2448                animation: sparkle-' . $calId . ' 1s ease-out forwards;
2449                filter: drop-shadow(0 0 3px rgba(255, 20, 147, 0.8));
2450            }
2451            </style>';
2452
2453            $html .= '<script>
2454            (function() {
2455                const container = document.getElementById("sidebar-widget-' . $calId . '");
2456                const sparkles = ["✨", "��", "��", "⭐", "��", "��", "��", "��", "��", "��"];
2457
2458                function createSparkle(x, y) {
2459                    const sparkle = document.createElement("div");
2460                    sparkle.className = "sparkle-' . $calId . '";
2461                    sparkle.textContent = sparkles[Math.floor(Math.random() * sparkles.length)];
2462                    sparkle.style.left = x + "px";
2463                    sparkle.style.top = y + "px";
2464
2465                    // Random direction
2466                    const angle = Math.random() * Math.PI * 2;
2467                    const distance = 30 + Math.random() * 40;
2468                    sparkle.style.setProperty("--tx", Math.cos(angle) * distance + "px");
2469                    sparkle.style.setProperty("--ty", Math.sin(angle) * distance + "px");
2470
2471                    container.appendChild(sparkle);
2472
2473                    setTimeout(() => sparkle.remove(), 1000);
2474                }
2475
2476                // Click sparkles
2477                container.addEventListener("click", function(e) {
2478                    const rect = container.getBoundingClientRect();
2479                    const x = e.clientX - rect.left;
2480                    const y = e.clientY - rect.top;
2481
2482                    // Create LOTS of sparkles for maximum bling!
2483                    for (let i = 0; i < 8; i++) {
2484                        setTimeout(() => {
2485                            const offsetX = x + (Math.random() - 0.5) * 30;
2486                            const offsetY = y + (Math.random() - 0.5) * 30;
2487                            createSparkle(offsetX, offsetY);
2488                        }, i * 40);
2489                    }
2490                });
2491
2492                // Random auto-sparkles for extra glamour
2493                setInterval(() => {
2494                    const x = Math.random() * container.offsetWidth;
2495                    const y = Math.random() * container.offsetHeight;
2496                    createSparkle(x, y);
2497                }, 3000);
2498            })();
2499            </script>';
2500        }
2501
2502        // Sanitize calId for use in JavaScript variable names (remove dashes)
2503        $jsCalId = str_replace('-', '_', $calId);
2504
2505        // CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it
2506        $html .= '<script>
2507(function() {
2508    // Shared state for system stats and tooltips
2509    const sharedState_' . $jsCalId . ' = {
2510        latestStats: {
2511            load: {"1min": 0, "5min": 0, "15min": 0},
2512            uptime: "",
2513            memory_details: {},
2514            top_processes: []
2515        },
2516        cpuHistory: [],
2517        CPU_HISTORY_SIZE: 2
2518    };
2519
2520    // Tooltip functions - MUST be defined before HTML uses them
2521    window["showTooltip_' . $jsCalId . '"] = function(color) {
2522        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
2523        if (!tooltip) {
2524            console.log("Tooltip element not found for color:", color);
2525            return;
2526        }
2527
2528        const latestStats = sharedState_' . $jsCalId . '.latestStats;
2529        let content = "";
2530
2531        if (color === "green") {
2532            content = "<div class=\\"tooltip-title\\">CPU Load Average</div>";
2533            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
2534            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
2535            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
2536            if (latestStats.uptime) {
2537                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_bright'] . ';\\">Uptime: " + latestStats.uptime + "</div>";
2538            }
2539            tooltip.style.setProperty("border-color", "' . $themeStyles['text_bright'] . '", "important");
2540            tooltip.style.setProperty("color", "' . $themeStyles['text_bright'] . '", "important");
2541            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_bright'] . '", "important");
2542        } else if (color === "purple") {
2543            content = "<div class=\\"tooltip-title\\">CPU Load (Short-term)</div>";
2544            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
2545            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
2546            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
2547                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['border'] . ';\\" class=\\"tooltip-title\\">Top Processes</div>";
2548                latestStats.top_processes.slice(0, 5).forEach(proc => {
2549                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
2550                });
2551            }
2552            tooltip.style.setProperty("border-color", "' . $themeStyles['border'] . '", "important");
2553            tooltip.style.setProperty("color", "' . $themeStyles['border'] . '", "important");
2554            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['border'] . '", "important");
2555        } else if (color === "orange") {
2556            content = "<div class=\\"tooltip-title\\">Memory Usage</div>";
2557            if (latestStats.memory_details && latestStats.memory_details.total) {
2558                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
2559                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
2560                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
2561                if (latestStats.memory_details.cached) {
2562                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
2563                }
2564            } else {
2565                content += "<div>Loading...</div>";
2566            }
2567            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
2568                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid ' . $themeStyles['text_primary'] . ';\\" class=\\"tooltip-title\\">Top Processes</div>";
2569                latestStats.top_processes.slice(0, 5).forEach(proc => {
2570                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
2571                });
2572            }
2573            tooltip.style.setProperty("border-color", "' . $themeStyles['text_primary'] . '", "important");
2574            tooltip.style.setProperty("color", "' . $themeStyles['text_primary'] . '", "important");
2575            tooltip.style.setProperty("-webkit-text-fill-color", "' . $themeStyles['text_primary'] . '", "important");
2576        }
2577
2578        tooltip.innerHTML = content;
2579        tooltip.style.setProperty("display", "block");
2580        tooltip.style.setProperty("background", "' . $themeStyles['bg'] . '", "important");
2581
2582        const bar = tooltip.parentElement;
2583        const barRect = bar.getBoundingClientRect();
2584        const tooltipRect = tooltip.getBoundingClientRect();
2585
2586        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
2587        const top = barRect.top - tooltipRect.height - 8;
2588
2589        tooltip.style.left = left + "px";
2590        tooltip.style.top = top + "px";
2591    };
2592
2593    window["hideTooltip_' . $jsCalId . '"] = function(color) {
2594        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
2595        if (tooltip) {
2596            tooltip.style.display = "none";
2597        }
2598    };
2599
2600    // Update clock every second
2601    function updateClock() {
2602        const now = new Date();
2603        let hours = now.getHours();
2604        const minutes = String(now.getMinutes()).padStart(2, "0");
2605        const seconds = String(now.getSeconds()).padStart(2, "0");
2606        const ampm = hours >= 12 ? "PM" : "AM";
2607        hours = hours % 12 || 12;
2608        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
2609        const clockEl = document.getElementById("clock-' . $calId . '");
2610        if (clockEl) clockEl.textContent = timeStr;
2611    }
2612    setInterval(updateClock, 1000);
2613
2614    // Weather - uses default location, click weather to get local
2615    var userLocationGranted = false;
2616    var userLat = 38.5816;  // Sacramento default
2617    var userLon = -121.4944;
2618
2619    function fetchWeatherData(lat, lon) {
2620        fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_weather=true&temperature_unit=fahrenheit")
2621            .then(response => response.json())
2622            .then(data => {
2623                if (data.current_weather) {
2624                    const temp = Math.round(data.current_weather.temperature);
2625                    const weatherCode = data.current_weather.weathercode;
2626                    const icon = getWeatherIcon(weatherCode);
2627                    const iconEl = document.getElementById("weather-icon-' . $calId . '");
2628                    const tempEl = document.getElementById("weather-temp-' . $calId . '");
2629                    if (iconEl) iconEl.textContent = icon;
2630                    if (tempEl) tempEl.innerHTML = temp + "&deg;";
2631                }
2632            })
2633            .catch(error => console.log("Weather fetch error:", error));
2634    }
2635
2636    function updateWeather() {
2637        fetchWeatherData(userLat, userLon);
2638    }
2639
2640    // Click weather icon to request local weather (user gesture required)
2641    function requestLocalWeather() {
2642        if (userLocationGranted) return;
2643        if ("geolocation" in navigator) {
2644            navigator.geolocation.getCurrentPosition(function(position) {
2645                userLat = position.coords.latitude;
2646                userLon = position.coords.longitude;
2647                userLocationGranted = true;
2648                fetchWeatherData(userLat, userLon);
2649            }, function(error) {
2650                console.log("Geolocation denied, using default location");
2651            });
2652        }
2653    }
2654
2655    setTimeout(function() {
2656        var weatherEl = document.querySelector("#weather-icon-' . $calId . '");
2657        if (weatherEl) {
2658            weatherEl.style.cursor = "pointer";
2659            weatherEl.title = "Click for local weather";
2660            weatherEl.addEventListener("click", requestLocalWeather);
2661        }
2662    }, 100);
2663
2664    function getWeatherIcon(code) {
2665        const icons = {
2666            0: "☀️", 1: "��️", 2: "⛅", 3: "☁️",
2667            45: "��️", 48: "��️", 51: "��️", 53: "��️", 55: "��️",
2668            61: "��️", 63: "��️", 65: "⛈️", 71: "��️", 73: "��️",
2669            75: "❄️", 77: "��️", 80: "��️", 81: "��️", 82: "⛈️",
2670            85: "��️", 86: "❄️", 95: "⛈️", 96: "⛈️", 99: "⛈️"
2671        };
2672        return icons[code] || "��️";
2673    }
2674
2675    // Update weather immediately and every 10 minutes
2676    updateWeather();
2677    setInterval(updateWeather, 600000);
2678
2679    // Update system stats and tooltips data
2680    function updateSystemStats() {
2681        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
2682            .then(response => response.json())
2683            .then(data => {
2684                sharedState_' . $jsCalId . '.latestStats = {
2685                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
2686                    uptime: data.uptime || "",
2687                    memory_details: data.memory_details || {},
2688                    top_processes: data.top_processes || []
2689                };
2690
2691                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
2692                if (greenBar) {
2693                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
2694                }
2695
2696                sharedState_' . $jsCalId . '.cpuHistory.push(data.cpu);
2697                if (sharedState_' . $jsCalId . '.cpuHistory.length > sharedState_' . $jsCalId . '.CPU_HISTORY_SIZE) {
2698                    sharedState_' . $jsCalId . '.cpuHistory.shift();
2699                }
2700
2701                const cpuAverage = sharedState_' . $jsCalId . '.cpuHistory.reduce((sum, val) => sum + val, 0) / sharedState_' . $jsCalId . '.cpuHistory.length;
2702
2703                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
2704                if (cpuBar) {
2705                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
2706                }
2707
2708                const memBar = document.getElementById("mem-realtime-' . $calId . '");
2709                if (memBar) {
2710                    memBar.style.width = Math.min(100, data.memory) + "%";
2711                }
2712            })
2713            .catch(error => {
2714                console.log("System stats error:", error);
2715            });
2716    }
2717
2718    updateSystemStats();
2719    setInterval(updateSystemStats, 2000);
2720})();
2721</script>';
2722
2723        // NOW add the header HTML (after JavaScript is defined)
2724        $todayDate = new DateTime();
2725        $displayDate = $todayDate->format('D, M j, Y');
2726        $currentTime = $todayDate->format('g:i:s A');
2727
2728        $html .= '<div class="eventlist-today-header" style="background:' . $themeStyles['header_bg'] . '; border:2px solid ' . $themeStyles['header_border'] . '; box-shadow:' . $themeStyles['header_shadow'] . ';">';
2729        $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '" style="color:' . $themeStyles['text_bright'] . ';">' . $currentTime . '</span>';
2730        $html .= '<div class="eventlist-bottom-info">';
2731        $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '" style="color:' . $themeStyles['text_primary'] . ';">--°</span></span>';
2732        $html .= '<span class="eventlist-today-date" style="color:' . $themeStyles['text_dim'] . ';">' . $displayDate . '</span>';
2733        $html .= '</div>';
2734
2735        // Three CPU/Memory bars (all update live)
2736        $html .= '<div class="eventlist-stats-container">';
2737
2738        // 5-minute load average (green, updates every 2 seconds)
2739        $html .= '<div class="eventlist-cpu-bar" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $jsCalId . '(\'green\')" onmouseout="hideTooltip_' . $jsCalId . '(\'green\')">';
2740        $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_bright'] . ' !important;"></div>';
2741        $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
2742        $html .= '</div>';
2743
2744        // Real-time CPU (purple, updates with 5-sec average)
2745        $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $jsCalId . '(\'purple\')" onmouseout="hideTooltip_' . $jsCalId . '(\'purple\')">';
2746        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['border'] . ' !important;"></div>';
2747        $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
2748        $html .= '</div>';
2749
2750        // Real-time Memory (orange, updates)
2751        $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" style="background:' . $themeStyles['cell_today_bg'] . ' !important;" onmouseover="showTooltip_' . $jsCalId . '(\'orange\')" onmouseout="hideTooltip_' . $jsCalId . '(\'orange\')">';
2752        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%; background:' . $themeStyles['text_primary'] . ' !important;"></div>';
2753        $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
2754        $html .= '</div>';
2755
2756        $html .= '</div>';
2757        $html .= '</div>';
2758
2759        // Get today's date for default event date
2760        $todayStr = date('Y-m-d');
2761
2762        // Thin "Add Event" bar between header and week grid - theme-aware colors
2763        $addBtnBg = $themeStyles['cell_today_bg'];
2764        $addBtnHover = $themeStyles['grid_bg'];
2765        $addBtnTextColor = ($theme === 'professional' || $theme === 'wiki') ?
2766                          $themeStyles['text_bright'] : $themeStyles['text_bright'];
2767        $addBtnShadow = ($theme === 'professional' || $theme === 'wiki') ?
2768                       '0 2px 4px rgba(0,0,0,0.2)' : '0 0 8px ' . $themeStyles['shadow'];
2769        $addBtnHoverShadow = ($theme === 'professional' || $theme === 'wiki') ?
2770                            '0 3px 6px rgba(0,0,0,0.3)' : '0 0 12px ' . $themeStyles['shadow'];
2771
2772        $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 . '\';">';
2773        $addBtnTextShadow = ($theme === 'pink') ? '0 0 3px ' . $addBtnTextColor : 'none';
2774        $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;">' . $this->getLang('add_event_short') . '</span>';
2775        $html .= '</div>';
2776
2777        // Week grid (7 cells)
2778        $html .= $this->renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme);
2779
2780        // Section colors - derived from theme palette
2781        // Today: brightest accent, Tomorrow: primary accent, Important: dim/secondary accent
2782        if ($theme === 'matrix') {
2783            $todayColor = '#00ff00';     // Bright green
2784            $tomorrowColor = '#00cc07';  // Standard green
2785            $importantColor = '#00aa00'; // Dim green
2786        } else if ($theme === 'purple') {
2787            $todayColor = '#d4a5ff';     // Bright purple
2788            $tomorrowColor = '#9b59b6';  // Standard purple
2789            $importantColor = '#8e7ab8'; // Dim purple
2790        } else if ($theme === 'pink') {
2791            $todayColor = '#ff1493';     // Hot pink
2792            $tomorrowColor = '#ff69b4';  // Medium pink
2793            $importantColor = '#ff85c1'; // Light pink
2794        } else if ($theme === 'professional') {
2795            $todayColor = '#4a90e2';     // Blue accent
2796            $tomorrowColor = '#5ba3e6';  // Lighter blue
2797            $importantColor = '#7fb8ec'; // Lightest blue
2798        } else {
2799            // Wiki - section header backgrounds from template colors
2800            $todayColor = $themeStyles['text_bright'];      // __link__
2801            $tomorrowColor = $themeStyles['header_bg'];     // __background_alt__
2802            $importantColor = $themeStyles['header_border'];// __border__
2803        }
2804
2805        // Check if there are any itinerary items
2806        $hasItinerary = !empty($todayEvents) || !empty($tomorrowEvents) || !empty($importantEvents);
2807
2808        // Itinerary bar (collapsible toggle) - styled like +Add bar
2809        $itineraryBg = $themeStyles['cell_today_bg'];
2810        $itineraryHover = $themeStyles['grid_bg'];
2811        $itineraryTextColor = ($theme === 'professional' || $theme === 'wiki') ?
2812                              $themeStyles['text_bright'] : $themeStyles['text_bright'];
2813        $itineraryShadow = ($theme === 'professional' || $theme === 'wiki') ?
2814                           '0 2px 4px rgba(0,0,0,0.2)' : '0 0 8px ' . $themeStyles['shadow'];
2815        $itineraryHoverShadow = ($theme === 'professional' || $theme === 'wiki') ?
2816                                '0 3px 6px rgba(0,0,0,0.3)' : '0 0 12px ' . $themeStyles['shadow'];
2817        $itineraryTextShadow = ($theme === 'pink') ? '0 0 3px ' . $itineraryTextColor : 'none';
2818
2819        // Sanitize calId for JavaScript
2820        $jsCalId = str_replace('-', '_', $calId);
2821
2822        // Get itinerary default state from settings
2823        $itineraryDefaultCollapsed = $this->getItineraryCollapsed();
2824        $arrowDefaultStyle = $itineraryDefaultCollapsed ? 'transform:rotate(-90deg);' : '';
2825        $contentDefaultStyle = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : '';
2826
2827        $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 . '\';">';
2828        $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>';
2829        $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>';
2830        $html .= '</div>';
2831
2832        // Itinerary content container (collapsible)
2833        $html .= '<div id="itinerary-content-' . $calId . '" style="transition:max-height 0.3s ease-out, opacity 0.2s ease-out; overflow:hidden; ' . $contentDefaultStyle . '">';
2834
2835        // Today section
2836        if (!empty($todayEvents)) {
2837            $html .= $this->renderSidebarSection($this->getLang('itinerary_today'), $todayEvents, $todayColor, $calId, $themeStyles, $theme, $importantNsList);
2838        }
2839
2840        // Tomorrow section
2841        if (!empty($tomorrowEvents)) {
2842            $html .= $this->renderSidebarSection($this->getLang('itinerary_tomorrow'), $tomorrowEvents, $tomorrowColor, $calId, $themeStyles, $theme, $importantNsList);
2843        }
2844
2845        // Important events section
2846        if (!empty($importantEvents)) {
2847            $html .= $this->renderSidebarSection($this->getLang('itinerary_important'), $importantEvents, $importantColor, $calId, $themeStyles, $theme, $importantNsList);
2848        }
2849
2850        // Empty state if no itinerary items
2851        if (!$hasItinerary) {
2852            $html .= '<div style="padding:8px; text-align:center; color:' . $themeStyles['text_dim'] . '; font-size:10px; font-family:system-ui, sans-serif;">' . $this->getLang('no_upcoming_events') . '</div>';
2853        }
2854
2855        $html .= '</div>'; // Close itinerary-content
2856
2857        // Get itinerary default state from settings
2858        $itineraryDefaultCollapsed = $this->getItineraryCollapsed();
2859        $itineraryExpandedDefault = $itineraryDefaultCollapsed ? 'false' : 'true';
2860        $itineraryArrowDefault = $itineraryDefaultCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
2861        $itineraryContentDefault = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : 'max-height:none;';
2862
2863        // JavaScript for toggling itinerary
2864        $html .= '<script>
2865        (function() {
2866            let itineraryExpanded_' . $jsCalId . ' = ' . $itineraryExpandedDefault . ';
2867
2868            window.toggleItinerary_' . $jsCalId . ' = function() {
2869                const content = document.getElementById("itinerary-content-' . $calId . '");
2870                const arrow = document.getElementById("itinerary-arrow-' . $calId . '");
2871
2872                if (itineraryExpanded_' . $jsCalId . ') {
2873                    // Collapse
2874                    content.style.maxHeight = "0px";
2875                    content.style.opacity = "0";
2876                    arrow.style.transform = "rotate(-90deg)";
2877                    itineraryExpanded_' . $jsCalId . ' = false;
2878                } else {
2879                    // Expand
2880                    content.style.maxHeight = content.scrollHeight + "px";
2881                    content.style.opacity = "1";
2882                    arrow.style.transform = "rotate(0deg)";
2883                    itineraryExpanded_' . $jsCalId . ' = true;
2884
2885                    // After transition, set to auto for dynamic content
2886                    setTimeout(function() {
2887                        if (itineraryExpanded_' . $jsCalId . ') {
2888                            content.style.maxHeight = "none";
2889                        }
2890                    }, 300);
2891                }
2892            };
2893
2894            // Initialize based on default state
2895            const content = document.getElementById("itinerary-content-' . $calId . '");
2896            const arrow = document.getElementById("itinerary-arrow-' . $calId . '");
2897            if (content && arrow) {
2898                if (' . $itineraryExpandedDefault . ') {
2899                    content.style.maxHeight = "none";
2900                    arrow.style.transform = "rotate(0deg)";
2901                } else {
2902                    content.style.maxHeight = "0px";
2903                    content.style.opacity = "0";
2904                    arrow.style.transform = "rotate(-90deg)";
2905                }
2906            }
2907        })();
2908        </script>';
2909
2910        $html .= '</div>';
2911
2912        // Add event dialog for sidebar widget
2913        $html .= $this->renderEventDialog($calId, $namespace, $theme);
2914
2915        // Add JavaScript for positioning data-tooltip elements
2916        $html .= '<script>
2917        // Position data-tooltip elements to prevent cutoff (up and to the LEFT)
2918        document.addEventListener("DOMContentLoaded", function() {
2919            const tooltipElements = document.querySelectorAll("[data-tooltip]");
2920            const isPinkTheme = document.querySelector(".sidebar-pink") !== null;
2921
2922            tooltipElements.forEach(function(element) {
2923                element.addEventListener("mouseenter", function() {
2924                    const rect = element.getBoundingClientRect();
2925                    const style = window.getComputedStyle(element, ":before");
2926
2927                    // Position above the element, aligned to LEFT (not right)
2928                    element.style.setProperty("--tooltip-left", (rect.left - 150) + "px");
2929                    element.style.setProperty("--tooltip-top", (rect.top - 30) + "px");
2930
2931                    // Pink theme: position heart to the right of tooltip
2932                    if (isPinkTheme) {
2933                        element.style.setProperty("--heart-left", (rect.left - 150 + 210) + "px");
2934                        element.style.setProperty("--heart-top", (rect.top - 30) + "px");
2935                    }
2936                });
2937            });
2938        });
2939
2940        // Apply custom properties to position tooltips
2941        const style = document.createElement("style");
2942        style.textContent = `
2943            [data-tooltip]:hover:before {
2944                left: var(--tooltip-left, 0) !important;
2945                top: var(--tooltip-top, 0) !important;
2946            }
2947            .sidebar-pink [data-tooltip]:hover:after {
2948                left: var(--heart-left, 0) !important;
2949                top: var(--heart-top, 0) !important;
2950            }
2951        `;
2952        document.head.appendChild(style);
2953        </script>';
2954
2955        return $html;
2956    }
2957
2958    /**
2959     * Render compact week grid (7 cells with event bars) - Theme-aware
2960     */
2961    private function renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme) {
2962        // Generate unique ID for this calendar instance - sanitize for JavaScript
2963        $calId = 'cal_' . substr(md5($weekStart . microtime()), 0, 8);
2964        $jsCalId = str_replace('-', '_', $calId);  // Sanitize for JS variable names
2965
2966        $html = '<div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:' . $themeStyles['grid_bg'] . '; border-bottom:2px solid ' . $themeStyles['grid_border'] . ';">';
2967
2968        // Day names depend on week start setting
2969        $weekStartDay = $this->getWeekStartDay();
2970        if ($weekStartDay === 'monday') {
2971            $dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];  // Monday to Sunday
2972        } else {
2973            $dayNames = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];  // Sunday to Saturday
2974        }
2975        $today = date('Y-m-d');
2976
2977        for ($i = 0; $i < 7; $i++) {
2978            $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days'));
2979            $dayNum = date('j', strtotime($date));
2980            $isToday = $date === $today;
2981
2982            $events = isset($weekEvents[$date]) ? $weekEvents[$date] : [];
2983            $eventCount = count($events);
2984
2985            $bgColor = $isToday ? $themeStyles['cell_today_bg'] : $themeStyles['cell_bg'];
2986            $textColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
2987            $fontWeight = $isToday ? '700' : '500';
2988
2989            // Theme-aware text shadow
2990            if ($theme === 'pink') {
2991                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
2992                $textShadow = $isToday ? 'text-shadow:0 0 3px ' . $glowColor . ';' : 'text-shadow:0 0 2px ' . $glowColor . ';';
2993            } else if ($theme === 'matrix') {
2994                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
2995                $textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glowColor . ';';
2996            } else if ($theme === 'purple') {
2997                $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary'];
2998                $textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glowColor . ';';
2999            } else {
3000                $textShadow = '';  // No glow for professional/wiki
3001            }
3002
3003            // Border color based on theme
3004            $borderColor = $themeStyles['grid_border'];
3005
3006            $hasEvents = $eventCount > 0;
3007            $clickableStyle = $hasEvents ? 'cursor:pointer;' : '';
3008            $clickHandler = $hasEvents ? ' onclick="showDayEvents_' . $jsCalId . '(\'' . $date . '\')"' : '';
3009
3010            $html .= '<div style="background:' . $bgColor . '; padding:4px 2px; text-align:center; min-height:45px; position:relative; border:1px solid ' . $borderColor . ' !important; ' . $clickableStyle . '" ' . $clickHandler . '>';
3011
3012            // Day letter - theme color
3013            $dayLetterColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary'];
3014            $html .= '<div style="font-size:9px; color:' . $dayLetterColor . '; font-weight:500; font-family:system-ui, sans-serif;">' . $dayNames[$i] . '</div>';
3015
3016            // Day number
3017            $html .= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:2px 0; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNum . '</div>';
3018
3019            // Event bars (max 4 visible) with theme-aware glow
3020            if ($eventCount > 0) {
3021                $showCount = min($eventCount, 4);
3022                for ($j = 0; $j < $showCount; $j++) {
3023                    $event = $events[$j];
3024                    $color = isset($event['color']) ? $event['color'] : $themeStyles['text_primary'];
3025                    $barShadow = $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . htmlspecialchars($color);
3026                    $html .= '<div style="height:2px; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:' . $barShadow . ';"></div>';
3027                }
3028
3029                // Show "+N more" if more than 4 - theme color
3030                if ($eventCount > 4) {
3031                    $moreTextColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary'];
3032                    $html .= '<div style="font-size:7px; color:' . $moreTextColor . '; margin-top:1px; font-family:system-ui, sans-serif;">+' . ($eventCount - 4) . '</div>';
3033                }
3034            }
3035
3036            $html .= '</div>';
3037        }
3038
3039        $html .= '</div>';
3040
3041        // Add container for selected day events display (with unique ID) - theme-aware
3042        $panelBorderColor = $themeStyles['border'];
3043        $panelHeaderBg = $themeStyles['border'];
3044        $panelShadow = ($theme === 'professional' || $theme === 'wiki') ?
3045                      '0 1px 3px rgba(0, 0, 0, 0.1)' :
3046                      '0 0 5px ' . $themeStyles['shadow'];
3047        $panelContentBg = ($theme === 'professional') ? 'rgba(255, 255, 255, 0.95)' :
3048                         ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.5)');
3049        $panelHeaderShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $panelHeaderBg;
3050
3051        // Header text color - dark bg text for dark themes, white for light theme accent headers
3052        $panelHeaderColor = ($theme === 'matrix' || $theme === 'purple' || $theme === 'pink') ? $themeStyles['bg'] :
3053                            (($theme === 'wiki') ? $themeStyles['text_primary'] : '#fff');
3054
3055        $html .= '<div id="selected-day-events-' . $calId . '" style="display:none; margin:8px 4px; border-left:3px solid ' . $panelBorderColor . ($theme === 'wiki' ? '' : ' !important') . '; box-shadow:' . $panelShadow . ';">';
3056        if ($theme === 'wiki') {
3057            $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;">';
3058            $html .= '<span id="selected-day-title-' . $calId . '"></span>';
3059            $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>';
3060        } else {
3061            $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;">';
3062            $html .= '<span id="selected-day-title-' . $calId . '"></span>';
3063            $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>';
3064        }
3065        $html .= '</div>';
3066        $html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:' . $panelContentBg . ';"></div>';
3067        $html .= '</div>';
3068
3069        // Add JavaScript for day selection with event data
3070        $html .= '<script>';
3071        // Sanitize calId for JavaScript variable names
3072        $jsCalId = str_replace('-', '_', $calId);
3073        $html .= 'window.weekEventsData_' . $jsCalId . ' = ' . json_encode($weekEvents) . ';';
3074
3075        // Pass theme colors to JavaScript
3076        $jsThemeColors = json_encode([
3077            'text_primary' => $themeStyles['text_primary'],
3078            'text_bright' => $themeStyles['text_bright'],
3079            'text_dim' => $themeStyles['text_dim'],
3080            'text_shadow' => ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $themeStyles['text_primary'] :
3081                             ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $themeStyles['text_primary'] : ''),
3082            'event_bg' => $theme === 'professional' ? 'rgba(255, 255, 255, 0.5)' :
3083                         ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.3)'),
3084            'border_color' => $theme === 'professional' ? 'rgba(0, 0, 0, 0.1)' :
3085                             ($theme === 'purple' ? 'rgba(155, 89, 182, 0.2)' :
3086                             ($theme === 'pink' ? 'rgba(255, 20, 147, 0.3)' :
3087                             ($theme === 'wiki' ? $themeStyles['grid_border'] : 'rgba(0, 204, 7, 0.2)'))),
3088            'bar_shadow' => $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' :
3089                           ($theme === 'wiki' ? '0 1px 2px rgba(0,0,0,0.15)' : '0 0 3px')
3090        ]);
3091        $html .= 'window.themeColors_' . $jsCalId . ' = ' . $jsThemeColors . ';';
3092        $html .= '
3093        window.showDayEvents_' . $jsCalId . ' = function(dateKey) {
3094            const eventsData = window.weekEventsData_' . $jsCalId . ';
3095            const container = document.getElementById("selected-day-events-' . $calId . '");
3096            const title = document.getElementById("selected-day-title-' . $calId . '");
3097            const content = document.getElementById("selected-day-content-' . $calId . '");
3098
3099            if (!eventsData[dateKey] || eventsData[dateKey].length === 0) return;
3100
3101            // Format date for display
3102            const dateObj = new Date(dateKey + "T00:00:00");
3103            const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" });
3104            const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" });
3105            title.textContent = dayName + ", " + monthDay;
3106
3107            // Clear content
3108            content.innerHTML = "";
3109
3110            // Sort events by time (all-day events first, then timed events chronologically)
3111            const sortedEvents = [...eventsData[dateKey]].sort((a, b) => {
3112                // All-day events (no time) go to the beginning
3113                if (!a.time && !b.time) return 0;
3114                if (!a.time) return -1;  // a is all-day, comes first
3115                if (!b.time) return 1;   // b is all-day, comes first
3116
3117                // Compare times (format: "HH:MM")
3118                const timeA = a.time.split(":").map(Number);
3119                const timeB = b.time.split(":").map(Number);
3120                const minutesA = timeA[0] * 60 + timeA[1];
3121                const minutesB = timeB[0] * 60 + timeB[1];
3122
3123                return minutesA - minutesB;
3124            });
3125
3126            // Build events HTML with single color bar (event color only) - theme-aware
3127            const themeColors = window.themeColors_' . $jsCalId . ';
3128            sortedEvents.forEach(event => {
3129                const eventColor = event.color || themeColors.text_primary;
3130
3131                const eventDiv = document.createElement("div");
3132                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;";
3133
3134                let eventHTML = "";
3135
3136                // Event assigned color bar (single bar on left) - theme-aware shadow
3137                const barShadow = themeColors.bar_shadow + (themeColors.bar_shadow.includes("rgba") ? "" : " " + eventColor);
3138                eventHTML += "<div style=\\"width:3px; align-self:stretch; background:" + eventColor + "; border-radius:1px; flex-shrink:0; box-shadow:" + barShadow + ";\\"></div>";
3139
3140                // Content wrapper
3141                eventHTML += "<div style=\\"flex:1; min-width:0; display:flex; justify-content:space-between; align-items:start; gap:4px;\\">";
3142
3143                // Left side: event details
3144                eventHTML += "<div style=\\"flex:1; min-width:0;\\">";
3145                eventHTML += "<div style=\\"font-weight:600; color:" + themeColors.text_primary + "; word-wrap:break-word; font-family:system-ui, sans-serif; " + themeColors.text_shadow + ";\\">";
3146
3147                // Time
3148                if (event.time) {
3149                    const timeParts = event.time.split(":");
3150                    let hours = parseInt(timeParts[0]);
3151                    const minutes = timeParts[1];
3152                    const ampm = hours >= 12 ? "PM" : "AM";
3153                    hours = hours % 12 || 12;
3154                    eventHTML += "<span style=\\"color:" + themeColors.text_bright + "; font-weight:500; font-size:9px;\\">" + hours + ":" + minutes + " " + ampm + "</span> ";
3155                }
3156
3157                // Title - use HTML version if available
3158                const titleHTML = event.title_html || event.title || "Untitled";
3159                eventHTML += titleHTML;
3160                eventHTML += "</div>";
3161
3162                // Description if present - use HTML version - theme-aware color
3163                if (event.description_html || event.description) {
3164                    const descHTML = event.description_html || event.description;
3165                    eventHTML += "<div style=\\"font-size:9px; color:" + themeColors.text_dim + "; margin-top:2px;\\">" + descHTML + "</div>";
3166                }
3167
3168                eventHTML += "</div>"; // Close event details
3169
3170                // Right side: conflict badge with tooltip
3171                if (event.conflict) {
3172                    let conflictList = [];
3173                    if (event.conflictingWith && event.conflictingWith.length > 0) {
3174                        event.conflictingWith.forEach(conf => {
3175                            const confTime = conf.time + (conf.end_time ? " - " + conf.end_time : "");
3176                            conflictList.push(conf.title + " (" + confTime + ")");
3177                        });
3178                    }
3179                    const conflictData = btoa(unescape(encodeURIComponent(JSON.stringify(conflictList))));
3180                    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>";
3181                }
3182
3183                eventHTML += "</div>"; // Close content wrapper
3184
3185                eventDiv.innerHTML = eventHTML;
3186                content.appendChild(eventDiv);
3187            });
3188
3189            container.style.display = "block";
3190        };
3191        ';
3192        $html .= '</script>';
3193
3194        return $html;
3195    }
3196
3197    /**
3198     * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders
3199     */
3200    private function renderSidebarSection($title, $events, $accentColor, $calId, $themeStyles, $theme, $importantNsList = ['important']) {
3201        // Keep the original accent colors for borders
3202        $borderColor = $accentColor;
3203
3204        // Show date for Important Events section
3205        $showDate = ($title === 'Important Events');
3206
3207        // Sort events differently based on section
3208        if ($title === 'Important Events') {
3209            // Important Events: sort by date first, then by time
3210            usort($events, function($a, $b) {
3211                $aDate = isset($a['date']) ? $a['date'] : '';
3212                $bDate = isset($b['date']) ? $b['date'] : '';
3213
3214                // Different dates - sort by date
3215                if ($aDate !== $bDate) {
3216                    return strcmp($aDate, $bDate);
3217                }
3218
3219                // Same date - sort by time
3220                $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : '';
3221                $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : '';
3222
3223                // All-day events last within same date
3224                if (empty($aTime) && !empty($bTime)) return 1;
3225                if (!empty($aTime) && empty($bTime)) return -1;
3226                if (empty($aTime) && empty($bTime)) return 0;
3227
3228                // Both have times
3229                $aMinutes = $this->timeToMinutes($aTime);
3230                $bMinutes = $this->timeToMinutes($bTime);
3231                return $aMinutes - $bMinutes;
3232            });
3233        } else {
3234            // Today/Tomorrow: sort by time only (all same date)
3235            usort($events, function($a, $b) {
3236                $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : '';
3237                $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : '';
3238
3239                // All-day events (no time) come first
3240                if (empty($aTime) && !empty($bTime)) return -1;
3241                if (!empty($aTime) && empty($bTime)) return 1;
3242                if (empty($aTime) && empty($bTime)) return 0;
3243
3244                // Both have times - convert to minutes for proper chronological sort
3245                $aMinutes = $this->timeToMinutes($aTime);
3246                $bMinutes = $this->timeToMinutes($bTime);
3247
3248                return $aMinutes - $bMinutes;
3249            });
3250        }
3251
3252        // Theme-aware section shadow
3253        $sectionShadow = ($theme === 'professional' || $theme === 'wiki') ?
3254                        '0 1px 3px rgba(0, 0, 0, 0.1)' :
3255                        '0 0 5px ' . $themeStyles['shadow'];
3256
3257        if ($theme === 'wiki') {
3258            // Wiki theme: use a background div for the left bar instead of border-left
3259            // Dark Reader maps border colors differently from background colors, causing mismatch
3260            $html = '<div style="display:flex; margin:8px 4px; box-shadow:' . $sectionShadow . '; background:' . $themeStyles['bg'] . ';">';
3261            $html .= '<div style="width:3px; flex-shrink:0; background:' . $borderColor . ';"></div>';
3262            $html .= '<div style="flex:1; min-width:0;">';
3263        } else {
3264            $html = '<div style="border-left:3px solid ' . $borderColor . ' !important; margin:8px 4px; box-shadow:' . $sectionShadow . ';">';
3265        }
3266
3267        // Section header with accent color background - theme-aware
3268        $headerShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $accentColor;
3269        $headerTextColor = ($theme === 'matrix' || $theme === 'purple' || $theme === 'pink') ? $themeStyles['bg'] :
3270                           (($theme === 'wiki') ? $themeStyles['text_primary'] : '#fff');
3271        if ($theme === 'wiki') {
3272            // Wiki theme: no !important — let Dark Reader adjust these
3273            $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 . ';">';
3274        } else {
3275            // Dark themes + professional: lock colors against Dark Reader
3276            $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 . ';">';
3277        }
3278        $html .= htmlspecialchars($title);
3279        $html .= '</div>';
3280
3281        // Events - no background (transparent)
3282        $html .= '<div style="padding:4px 0;">';
3283
3284        foreach ($events as $event) {
3285            $html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor, $themeStyles, $theme, $importantNsList);
3286        }
3287
3288        $html .= '</div>';
3289        $html .= '</div>';
3290        if ($theme === 'wiki') {
3291            $html .= '</div>'; // Close flex wrapper
3292        }
3293
3294        return $html;
3295    }
3296
3297    /**
3298     * Render individual event in sidebar - Theme-aware
3299     */
3300    private function renderSidebarEvent($event, $calId, $showDate = false, $sectionColor = '#00cc07', $themeStyles = null, $theme = 'matrix', $importantNsList = ['important']) {
3301        $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
3302        $time = isset($event['time']) ? $event['time'] : '';
3303        $endTime = isset($event['endTime']) ? $event['endTime'] : '';
3304        $eventColor = isset($event['color']) ? htmlspecialchars($event['color']) : ($themeStyles ? $themeStyles['text_primary'] : '#00cc07');
3305        $date = isset($event['date']) ? $event['date'] : '';
3306        $isTask = isset($event['isTask']) && $event['isTask'];
3307        $completed = isset($event['completed']) && $event['completed'];
3308
3309        // Check if this is an important namespace event
3310        $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
3311        $isImportantNs = false;
3312        foreach ($importantNsList as $impNs) {
3313            if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
3314                $isImportantNs = true;
3315                break;
3316            }
3317        }
3318
3319        // Theme-aware colors
3320        $titleColor = $themeStyles ? $themeStyles['text_primary'] : '#00cc07';
3321        $timeColor = $themeStyles ? $themeStyles['text_bright'] : '#00dd00';
3322        $textShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $titleColor . ';' :
3323                      ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $titleColor . ';' : '');
3324
3325        // Check for conflicts (using 'conflict' field set by detectTimeConflicts)
3326        $hasConflict = isset($event['conflict']) && $event['conflict'];
3327        $conflictingWith = isset($event['conflictingWith']) ? $event['conflictingWith'] : [];
3328
3329        // Build conflict list for tooltip
3330        $conflictList = [];
3331        if ($hasConflict && !empty($conflictingWith)) {
3332            foreach ($conflictingWith as $conf) {
3333                $confTime = $this->formatTimeDisplay($conf['time'], isset($conf['end_time']) ? $conf['end_time'] : '');
3334                $conflictList[] = $conf['title'] . ' (' . $confTime . ')';
3335            }
3336        }
3337
3338        // No background on individual events (transparent) - unless important namespace
3339        // Use theme grid_border with slight opacity for subtle divider
3340        $borderColor = $themeStyles['grid_border'];
3341
3342        // Important namespace highlighting - subtle themed background
3343        $importantBg = '';
3344        $importantBorder = '';
3345        if ($isImportantNs) {
3346            // Theme-specific important highlighting
3347            switch ($theme) {
3348                case 'matrix':
3349                    $importantBg = 'background:rgba(0,204,7,0.08);';
3350                    $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);';
3351                    break;
3352                case 'purple':
3353                    $importantBg = 'background:rgba(156,39,176,0.08);';
3354                    $importantBorder = 'border-right:2px solid rgba(156,39,176,0.4);';
3355                    break;
3356                case 'pink':
3357                    $importantBg = 'background:rgba(255,105,180,0.1);';
3358                    $importantBorder = 'border-right:2px solid rgba(255,105,180,0.5);';
3359                    break;
3360                case 'professional':
3361                    $importantBg = 'background:rgba(33,150,243,0.08);';
3362                    $importantBorder = 'border-right:2px solid rgba(33,150,243,0.4);';
3363                    break;
3364                case 'wiki':
3365                    $importantBg = 'background:rgba(0,102,204,0.06);';
3366                    $importantBorder = 'border-right:2px solid rgba(0,102,204,0.3);';
3367                    break;
3368                default:
3369                    $importantBg = 'background:rgba(0,204,7,0.08);';
3370                    $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);';
3371            }
3372        }
3373
3374        $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 . '">';
3375
3376        // Event's assigned color bar (single bar on the left)
3377        $barShadow = ($theme === 'professional') ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . $eventColor;
3378        $html .= '<div style="width:3px; align-self:stretch; background:' . $eventColor . '; border-radius:1px; flex-shrink:0; box-shadow:' . $barShadow . ';"></div>';
3379
3380        // Content
3381        $html .= '<div style="flex:1; min-width:0;">';
3382
3383        // Time + title
3384        $html .= '<div style="font-weight:600; color:' . $titleColor . '; word-wrap:break-word; font-family:system-ui, sans-serif; ' . $textShadow . '">';
3385
3386        if ($time) {
3387            $displayTime = $this->formatTimeDisplay($time, $endTime);
3388            $html .= '<span style="color:' . $timeColor . '; font-weight:500; font-size:9px;">' . htmlspecialchars($displayTime) . '</span> ';
3389        }
3390
3391        // Task checkbox
3392        if ($isTask) {
3393            $checkIcon = $completed ? '☑' : '☐';
3394            $checkColor = $themeStyles ? $themeStyles['text_bright'] : '#00ff00';
3395            $html .= '<span style="font-size:11px; color:' . $checkColor . ';">' . $checkIcon . '</span> ';
3396        }
3397
3398        // Important indicator icon for important namespace events
3399        if ($isImportantNs) {
3400            $html .= '<span style="font-size:9px;" title="Important">⭐</span> ';
3401        }
3402
3403        $html .= $title; // Already HTML-escaped on line 2625
3404
3405        // Conflict badge using same system as main calendar
3406        if ($hasConflict && !empty($conflictList)) {
3407            $conflictJson = base64_encode(json_encode($conflictList));
3408            $html .= ' <span class="event-conflict-badge" style="font-size:10px;" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . count($conflictList) . '</span>';
3409        }
3410
3411        $html .= '</div>';
3412
3413        // Date display BELOW event name for Important events
3414        if ($showDate && $date) {
3415            $dateObj = new DateTime($date);
3416            $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10"
3417            $dateColor = $themeStyles ? $themeStyles['text_dim'] : '#00aa00';
3418            $dateShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $dateColor . ';' :
3419                          ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $dateColor . ';' : '');
3420            $html .= '<div style="font-size:8px; color:' . $dateColor . '; font-weight:500; margin-top:2px; ' . $dateShadow . '">' . htmlspecialchars($displayDate) . '</div>';
3421        }
3422
3423        $html .= '</div>';
3424        $html .= '</div>';
3425
3426        return $html;
3427    }
3428
3429    /**
3430     * Format time display (12-hour format with optional end time)
3431     */
3432    private function formatTimeDisplay($startTime, $endTime = '') {
3433        // Convert start time
3434        list($hour, $minute) = explode(':', $startTime);
3435        $hour = (int)$hour;
3436        $ampm = $hour >= 12 ? 'PM' : 'AM';
3437        $displayHour = $hour % 12;
3438        if ($displayHour === 0) $displayHour = 12;
3439
3440        $display = $displayHour . ':' . $minute . ' ' . $ampm;
3441
3442        // Add end time if provided
3443        if ($endTime && $endTime !== '') {
3444            list($endHour, $endMinute) = explode(':', $endTime);
3445            $endHour = (int)$endHour;
3446            $endAmpm = $endHour >= 12 ? 'PM' : 'AM';
3447            $endDisplayHour = $endHour % 12;
3448            if ($endDisplayHour === 0) $endDisplayHour = 12;
3449
3450            $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm;
3451        }
3452
3453        return $display;
3454    }
3455
3456    /**
3457     * Detect time conflicts among events on the same day
3458     * Returns events array with 'conflict' flag and 'conflictingWith' array
3459     */
3460    private function detectTimeConflicts($dayEvents) {
3461        if (empty($dayEvents)) {
3462            return $dayEvents;
3463        }
3464
3465        // If only 1 event, no conflicts possible but still add the flag
3466        if (count($dayEvents) === 1) {
3467            return [array_merge($dayEvents[0], ['conflict' => false, 'conflictingWith' => []])];
3468        }
3469
3470        $eventsWithFlags = [];
3471
3472        foreach ($dayEvents as $i => $event) {
3473            $hasConflict = false;
3474            $conflictingWith = [];
3475
3476            // Skip all-day events (no time)
3477            if (empty($event['time'])) {
3478                $eventsWithFlags[] = array_merge($event, ['conflict' => false, 'conflictingWith' => []]);
3479                continue;
3480            }
3481
3482            // Get this event's time range
3483            $startTime = $event['time'];
3484            // Check both 'end_time' (snake_case) and 'endTime' (camelCase) for compatibility
3485            $endTime = '';
3486            if (isset($event['end_time']) && $event['end_time'] !== '') {
3487                $endTime = $event['end_time'];
3488            } elseif (isset($event['endTime']) && $event['endTime'] !== '') {
3489                $endTime = $event['endTime'];
3490            } else {
3491                // If no end time, use start time (zero duration) - matches main calendar logic
3492                $endTime = $startTime;
3493            }
3494
3495            // Check against all other events
3496            foreach ($dayEvents as $j => $otherEvent) {
3497                if ($i === $j) continue; // Skip self
3498                if (empty($otherEvent['time'])) continue; // Skip all-day events
3499
3500                $otherStart = $otherEvent['time'];
3501                // Check both field name formats
3502                $otherEnd = '';
3503                if (isset($otherEvent['end_time']) && $otherEvent['end_time'] !== '') {
3504                    $otherEnd = $otherEvent['end_time'];
3505                } elseif (isset($otherEvent['endTime']) && $otherEvent['endTime'] !== '') {
3506                    $otherEnd = $otherEvent['endTime'];
3507                } else {
3508                    $otherEnd = $otherStart;
3509                }
3510
3511                // Check for overlap: convert to minutes and compare
3512                $start1Min = $this->timeToMinutes($startTime);
3513                $end1Min = $this->timeToMinutes($endTime);
3514                $start2Min = $this->timeToMinutes($otherStart);
3515                $end2Min = $this->timeToMinutes($otherEnd);
3516
3517                // Overlap if: start1 < end2 AND start2 < end1
3518                // Note: Using < (not <=) so events that just touch at boundaries don't conflict
3519                // e.g., 1:00-2:00 and 2:00-3:00 are NOT in conflict
3520                if ($start1Min < $end2Min && $start2Min < $end1Min) {
3521                    $hasConflict = true;
3522                    $conflictingWith[] = [
3523                        'title' => isset($otherEvent['title']) ? $otherEvent['title'] : 'Untitled',
3524                        'time' => $otherStart,
3525                        'end_time' => $otherEnd
3526                    ];
3527                }
3528            }
3529
3530            $eventsWithFlags[] = array_merge($event, [
3531                'conflict' => $hasConflict,
3532                'conflictingWith' => $conflictingWith
3533            ]);
3534        }
3535
3536        return $eventsWithFlags;
3537    }
3538
3539    /**
3540     * Add hours to a time string
3541     */
3542    private function addHoursToTime($time, $hours) {
3543        $totalMinutes = $this->timeToMinutes($time) + ($hours * 60);
3544        $h = floor($totalMinutes / 60) % 24;
3545        $m = $totalMinutes % 60;
3546        return sprintf('%02d:%02d', $h, $m);
3547    }
3548
3549    /**
3550     * Render DokuWiki syntax to HTML
3551     * Converts **bold**, //italic//, [[links]], etc. to HTML
3552     */
3553    private function renderDokuWikiToHtml($text) {
3554        if (empty($text)) return '';
3555
3556        // Use DokuWiki's parser to render the text
3557        $instructions = p_get_instructions($text);
3558
3559        // Render instructions to XHTML
3560        $xhtml = p_render('xhtml', $instructions, $info);
3561
3562        // Remove surrounding <p> tags if present (we're rendering inline)
3563        $xhtml = preg_replace('/^<p>(.*)<\/p>$/s', '$1', trim($xhtml));
3564
3565        return $xhtml;
3566    }
3567
3568    // Keep old scanForNamespaces for backward compatibility (not used anymore)
3569    private function scanForNamespaces($dir, $baseNamespace, &$namespaces) {
3570        if (!is_dir($dir)) return;
3571
3572        $items = scandir($dir);
3573        foreach ($items as $item) {
3574            if ($item === '.' || $item === '..' || $item === 'calendar') continue;
3575
3576            $path = $dir . $item;
3577            if (is_dir($path)) {
3578                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
3579                $namespaces[] = $namespace;
3580                $this->scanForNamespaces($path . '/', $namespace, $namespaces);
3581            }
3582        }
3583    }
3584
3585    /**
3586     * Get current sidebar theme
3587     */
3588    private function getSidebarTheme() {
3589        $configFile = DOKU_INC . 'data/meta/calendar_theme.txt';
3590        if (file_exists($configFile)) {
3591            $theme = trim(file_get_contents($configFile));
3592            if (in_array($theme, ['matrix', 'purple', 'professional', 'pink', 'wiki'])) {
3593                return $theme;
3594            }
3595        }
3596        return 'matrix'; // Default
3597    }
3598
3599    /**
3600     * Get colors from DokuWiki template's style.ini file
3601     */
3602    private function getWikiTemplateColors() {
3603        global $conf;
3604
3605        // Get current template name
3606        $template = $conf['template'];
3607
3608        // Try multiple possible locations for style.ini
3609        $possiblePaths = [
3610            DOKU_INC . 'conf/tpl/' . $template . '/style.ini',
3611            DOKU_INC . 'lib/tpl/' . $template . '/style.ini',
3612        ];
3613
3614        $styleIni = null;
3615        foreach ($possiblePaths as $path) {
3616            if (file_exists($path)) {
3617                $styleIni = parse_ini_file($path, true);
3618                break;
3619            }
3620        }
3621
3622        if (!$styleIni) {
3623            return null; // Fall back to CSS variables
3624        }
3625
3626        // Extract color replacements
3627        $replacements = isset($styleIni['replacements']) ? $styleIni['replacements'] : [];
3628
3629        // Map style.ini colors to our theme structure
3630        $bgSite = isset($replacements['__background_site__']) ? $replacements['__background_site__'] : '#f5f5f5';
3631        $background = isset($replacements['__background__']) ? $replacements['__background__'] : '#fff';
3632        $bgAlt = isset($replacements['__background_alt__']) ? $replacements['__background_alt__'] : '#e8e8e8';
3633        $bgNeu = isset($replacements['__background_neu__']) ? $replacements['__background_neu__'] : '#eee';
3634        $text = isset($replacements['__text__']) ? $replacements['__text__'] : '#333';
3635        $textAlt = isset($replacements['__text_alt__']) ? $replacements['__text_alt__'] : '#999';
3636        $textNeu = isset($replacements['__text_neu__']) ? $replacements['__text_neu__'] : '#666';
3637        $border = isset($replacements['__border__']) ? $replacements['__border__'] : '#ccc';
3638        $link = isset($replacements['__link__']) ? $replacements['__link__'] : '#2b73b7';
3639        $existing = isset($replacements['__existing__']) ? $replacements['__existing__'] : $link;
3640
3641        // Build theme colors from template colors
3642        // ============================================
3643        // DokuWiki style.ini → Calendar CSS Variable Mapping
3644        // ============================================
3645        //   style.ini key         → CSS variable          → Used for
3646        //   __background_site__   → --background-site     → Container, panel backgrounds
3647        //   __background__        → --cell-bg             → Cell/input backgrounds (typically white)
3648        //   __background_alt__    → --background-alt      → Hover states, header backgrounds
3649        //                         → --background-header
3650        //   __background_neu__    → --cell-today-bg       → Today cell highlight
3651        //   __text__              → --text-primary        → Primary text, labels, titles
3652        //   __text_neu__          → --text-dim            → Secondary text, dates, descriptions
3653        //   __text_alt__          → (not mapped)          → Available for future use
3654        //   __border__            → --border-color        → Grid lines, input borders
3655        //                         → --border-main         → Accent color: buttons, badges, active elements, section headers
3656        //                         → --header-border
3657        //   __link__              → --text-bright         → Links, accent text
3658        //   __existing__          → (fallback to __link__)→ Available for future use
3659        //
3660        // To customize: edit your template's conf/style.ini [replacements]
3661        return [
3662            'bg' => $bgSite,
3663            'border' => $border,         // Accent color from template border
3664            'shadow' => 'rgba(0, 0, 0, 0.1)',
3665            'header_bg' => $bgAlt,       // Headers use alt background
3666            'header_border' => $border,
3667            'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
3668            'text_primary' => $text,
3669            'text_bright' => $link,
3670            'text_dim' => $textNeu,
3671            'grid_bg' => $bgSite,
3672            'grid_border' => $border,
3673            'cell_bg' => $background,    // Cells use __background__ (white/light)
3674            'cell_today_bg' => $bgNeu,
3675            'bar_glow' => '0 1px 2px',
3676            'pastdue_color' => '#e74c3c',
3677            'pastdue_bg' => '#ffe6e6',
3678            'pastdue_bg_strong' => '#ffd9d9',
3679            'pastdue_bg_light' => '#fff2f2',
3680            'tomorrow_bg' => '#fff9e6',
3681            'tomorrow_bg_strong' => '#fff4cc',
3682            'tomorrow_bg_light' => '#fffbf0',
3683        ];
3684    }
3685
3686    /**
3687     * Get theme-specific color styles
3688     */
3689    private function getSidebarThemeStyles($theme) {
3690        // For wiki theme, try to read colors from template's style.ini
3691        if ($theme === 'wiki') {
3692            $wikiColors = $this->getWikiTemplateColors();
3693            if (!empty($wikiColors)) {
3694                return $wikiColors;
3695            }
3696            // Fall through to default wiki colors if reading fails
3697        }
3698
3699        $themes = [
3700            'matrix' => [
3701                'bg' => '#242424',
3702                'border' => '#00cc07',
3703                'shadow' => 'rgba(0, 204, 7, 0.3)',
3704                'header_bg' => 'linear-gradient(180deg, #2a2a2a 0%, #242424 100%)',
3705                'header_border' => '#00cc07',
3706                'header_shadow' => '0 2px 8px rgba(0, 204, 7, 0.3)',
3707                'text_primary' => '#00cc07',
3708                'text_bright' => '#00ff00',
3709                'text_dim' => '#00aa00',
3710                'grid_bg' => '#1a3d1a',
3711                'grid_border' => '#00cc07',
3712                'cell_bg' => '#242424',
3713                'cell_today_bg' => '#2a4d2a',
3714                'bar_glow' => '0 0 3px',
3715                'pastdue_color' => '#e74c3c',
3716                'pastdue_bg' => '#3d1a1a',
3717                'pastdue_bg_strong' => '#4d2020',
3718                'pastdue_bg_light' => '#2d1515',
3719                'tomorrow_bg' => '#3d3d1a',
3720                'tomorrow_bg_strong' => '#4d4d20',
3721                'tomorrow_bg_light' => '#2d2d15',
3722            ],
3723            'purple' => [
3724                'bg' => '#2a2030',
3725                'border' => '#9b59b6',
3726                'shadow' => 'rgba(155, 89, 182, 0.3)',
3727                'header_bg' => 'linear-gradient(180deg, #2f2438 0%, #2a2030 100%)',
3728                'header_border' => '#9b59b6',
3729                'header_shadow' => '0 2px 8px rgba(155, 89, 182, 0.3)',
3730                'text_primary' => '#b19cd9',
3731                'text_bright' => '#d4a5ff',
3732                'text_dim' => '#8e7ab8',
3733                'grid_bg' => '#3d2b4d',
3734                'grid_border' => '#9b59b6',
3735                'cell_bg' => '#2a2030',
3736                'cell_today_bg' => '#3d2b4d',
3737                'bar_glow' => '0 0 3px',
3738                'pastdue_color' => '#e74c3c',
3739                'pastdue_bg' => '#3d1a2a',
3740                'pastdue_bg_strong' => '#4d2035',
3741                'pastdue_bg_light' => '#2d1520',
3742                'tomorrow_bg' => '#3d3520',
3743                'tomorrow_bg_strong' => '#4d4028',
3744                'tomorrow_bg_light' => '#2d2a18',
3745            ],
3746            'professional' => [
3747                'bg' => '#f5f7fa',
3748                'border' => '#4a90e2',
3749                'shadow' => 'rgba(74, 144, 226, 0.2)',
3750                'header_bg' => 'linear-gradient(180deg, #ffffff 0%, #f5f7fa 100%)',
3751                'header_border' => '#4a90e2',
3752                'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
3753                'text_primary' => '#2c3e50',
3754                'text_bright' => '#4a90e2',
3755                'text_dim' => '#7f8c8d',
3756                'grid_bg' => '#e8ecf1',
3757                'grid_border' => '#d0d7de',
3758                'cell_bg' => '#ffffff',
3759                'cell_today_bg' => '#dce8f7',
3760                'bar_glow' => '0 1px 2px',
3761                'pastdue_color' => '#e74c3c',
3762                'pastdue_bg' => '#ffe6e6',
3763                'pastdue_bg_strong' => '#ffd9d9',
3764                'pastdue_bg_light' => '#fff2f2',
3765                'tomorrow_bg' => '#fff9e6',
3766                'tomorrow_bg_strong' => '#fff4cc',
3767                'tomorrow_bg_light' => '#fffbf0',
3768            ],
3769            'pink' => [
3770                'bg' => '#1a0d14',
3771                'border' => '#ff1493',
3772                'shadow' => 'rgba(255, 20, 147, 0.4)',
3773                'header_bg' => 'linear-gradient(180deg, #2d1a24 0%, #1a0d14 100%)',
3774                'header_border' => '#ff1493',
3775                'header_shadow' => '0 0 12px rgba(255, 20, 147, 0.6)',
3776                'text_primary' => '#ff69b4',
3777                'text_bright' => '#ff1493',
3778                'text_dim' => '#ff85c1',
3779                'grid_bg' => '#2d1a24',
3780                'grid_border' => '#ff1493',
3781                'cell_bg' => '#1a0d14',
3782                'cell_today_bg' => '#3d2030',
3783                'bar_glow' => '0 0 5px',
3784                'pastdue_color' => '#e74c3c',
3785                'pastdue_bg' => '#3d1520',
3786                'pastdue_bg_strong' => '#4d1a28',
3787                'pastdue_bg_light' => '#2d1018',
3788                'tomorrow_bg' => '#3d3020',
3789                'tomorrow_bg_strong' => '#4d3a28',
3790                'tomorrow_bg_light' => '#2d2518',
3791            ],
3792            'wiki' => [
3793                'bg' => '#f5f5f5',
3794                'border' => '#ccc',          // Template __border__ color
3795                'shadow' => 'rgba(0, 0, 0, 0.1)',
3796                'header_bg' => '#e8e8e8',
3797                'header_border' => '#ccc',
3798                'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)',
3799                'text_primary' => '#333',
3800                'text_bright' => '#2b73b7',  // Template __link__ color
3801                'text_dim' => '#666',
3802                'grid_bg' => '#f5f5f5',
3803                'grid_border' => '#ccc',
3804                'cell_bg' => '#fff',
3805                'cell_today_bg' => '#eee',
3806                'bar_glow' => '0 1px 2px',
3807                'pastdue_color' => '#e74c3c',
3808                'pastdue_bg' => '#ffe6e6',
3809                'pastdue_bg_strong' => '#ffd9d9',
3810                'pastdue_bg_light' => '#fff2f2',
3811                'tomorrow_bg' => '#fff9e6',
3812                'tomorrow_bg_strong' => '#fff4cc',
3813                'tomorrow_bg_light' => '#fffbf0',
3814            ],
3815        ];
3816
3817        return isset($themes[$theme]) ? $themes[$theme] : $themes['matrix'];
3818    }
3819
3820    /**
3821     * Get week start day preference
3822     */
3823    private function getWeekStartDay() {
3824        $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt';
3825        if (file_exists($configFile)) {
3826            $start = trim(file_get_contents($configFile));
3827            if (in_array($start, ['monday', 'sunday'])) {
3828                return $start;
3829            }
3830        }
3831        return 'sunday'; // Default to Sunday (US/Canada standard)
3832    }
3833
3834    /**
3835     * Get itinerary collapsed default state
3836     */
3837    private function getItineraryCollapsed() {
3838        $configFile = DOKU_INC . 'data/meta/calendar_itinerary_collapsed.txt';
3839        if (file_exists($configFile)) {
3840            return trim(file_get_contents($configFile)) === 'yes';
3841        }
3842        return false; // Default to expanded
3843    }
3844
3845    /**
3846     * Get language strings needed by JavaScript
3847     */
3848    private function getJsLangStrings() {
3849        return [
3850            // Month names (full)
3851            'month_january' => $this->getLang('month_january'),
3852            'month_february' => $this->getLang('month_february'),
3853            'month_march' => $this->getLang('month_march'),
3854            'month_april' => $this->getLang('month_april'),
3855            'month_may' => $this->getLang('month_may'),
3856            'month_june' => $this->getLang('month_june'),
3857            'month_july' => $this->getLang('month_july'),
3858            'month_august' => $this->getLang('month_august'),
3859            'month_september' => $this->getLang('month_september'),
3860            'month_october' => $this->getLang('month_october'),
3861            'month_november' => $this->getLang('month_november'),
3862            'month_december' => $this->getLang('month_december'),
3863
3864            // Month names (short)
3865            'month_jan' => $this->getLang('month_jan'),
3866            'month_feb' => $this->getLang('month_feb'),
3867            'month_mar' => $this->getLang('month_mar'),
3868            'month_apr' => $this->getLang('month_apr'),
3869            'month_may_short' => $this->getLang('month_may_short'),
3870            'month_jun' => $this->getLang('month_jun'),
3871            'month_jul' => $this->getLang('month_jul'),
3872            'month_aug' => $this->getLang('month_aug'),
3873            'month_sep' => $this->getLang('month_sep'),
3874            'month_oct' => $this->getLang('month_oct'),
3875            'month_nov' => $this->getLang('month_nov'),
3876            'month_dec' => $this->getLang('month_dec'),
3877
3878            // Day names (short)
3879            'day_sun' => $this->getLang('day_sun'),
3880            'day_mon' => $this->getLang('day_mon'),
3881            'day_tue' => $this->getLang('day_tue'),
3882            'day_wed' => $this->getLang('day_wed'),
3883            'day_thu' => $this->getLang('day_thu'),
3884            'day_fri' => $this->getLang('day_fri'),
3885            'day_sat' => $this->getLang('day_sat'),
3886
3887            // UI labels
3888            'events_header' => $this->getLang('events_header'),
3889            'events_for_date' => $this->getLang('events_for_date'),
3890            'past_events' => $this->getLang('past_events'),
3891            'no_events_day' => $this->getLang('no_events_day'),
3892            'no_events_month' => $this->getLang('no_events_month'),
3893            'add_btn' => $this->getLang('add_btn'),
3894            'add_event_btn' => $this->getLang('add_event_btn'),
3895            'click_to_jump' => $this->getLang('click_to_jump'),
3896
3897            // Dialog titles
3898            'dialog_add_event' => $this->getLang('dialog_add_event'),
3899            'dialog_edit_event' => $this->getLang('dialog_edit_event'),
3900
3901            // Confirmations
3902            'delete_event_confirm' => $this->getLang('delete_event_confirm'),
3903
3904            // Badges
3905            'badge_past_due' => $this->getLang('badge_past_due'),
3906            'badge_today' => $this->getLang('badge_today'),
3907
3908            // Tooltips
3909            'important_tooltip' => $this->getLang('important_tooltip'),
3910
3911            // Default text
3912            'default_event' => $this->getLang('default_event'),
3913        ];
3914    }
3915}