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