119378907SAtari911<?php 219378907SAtari911/** 319378907SAtari911 * DokuWiki Plugin calendar (Syntax Component) 419378907SAtari911 * Compact design with integrated event list 519378907SAtari911 * 619378907SAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 719378907SAtari911 * @author DokuWiki Community 819378907SAtari911 */ 919378907SAtari911 1019378907SAtari911if (!defined('DOKU_INC')) die(); 1119378907SAtari911 1219378907SAtari911class syntax_plugin_calendar extends DokuWiki_Syntax_Plugin { 1319378907SAtari911 1419378907SAtari911 public function getType() { 1519378907SAtari911 return 'substition'; 1619378907SAtari911 } 1719378907SAtari911 1819378907SAtari911 public function getPType() { 1919378907SAtari911 return 'block'; 2019378907SAtari911 } 2119378907SAtari911 2219378907SAtari911 public function getSort() { 2319378907SAtari911 return 155; 2419378907SAtari911 } 2519378907SAtari911 2619378907SAtari911 public function connectTo($mode) { 2719378907SAtari911 $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 2819378907SAtari911 $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 2919378907SAtari911 $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 3019378907SAtari911 } 3119378907SAtari911 3219378907SAtari911 public function handle($match, $state, $pos, Doku_Handler $handler) { 3319378907SAtari911 $isEventList = (strpos($match, '{{eventlist') === 0); 3419378907SAtari911 $isEventPanel = (strpos($match, '{{eventpanel') === 0); 3519378907SAtari911 3619378907SAtari911 if ($isEventList) { 3719378907SAtari911 $match = substr($match, 12, -2); 3819378907SAtari911 } elseif ($isEventPanel) { 3919378907SAtari911 $match = substr($match, 13, -2); 4019378907SAtari911 } else { 4119378907SAtari911 $match = substr($match, 10, -2); 4219378907SAtari911 } 4319378907SAtari911 4419378907SAtari911 $params = array( 4519378907SAtari911 'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'), 4619378907SAtari911 'year' => date('Y'), 4719378907SAtari911 'month' => date('n'), 4819378907SAtari911 'namespace' => '', 4919378907SAtari911 'daterange' => '', 50e3a9f44cSAtari911 'date' => '', 51e3a9f44cSAtari911 'range' => '' 5219378907SAtari911 ); 5319378907SAtari911 5419378907SAtari911 if (trim($match)) { 5519378907SAtari911 $pairs = preg_split('/\s+/', trim($match)); 5619378907SAtari911 foreach ($pairs as $pair) { 5719378907SAtari911 if (strpos($pair, '=') !== false) { 5819378907SAtari911 list($key, $value) = explode('=', $pair, 2); 5919378907SAtari911 $params[trim($key)] = trim($value); 6087ac9bf3SAtari911 } else { 6187ac9bf3SAtari911 // Handle standalone flags like "today" 6287ac9bf3SAtari911 $params[trim($pair)] = true; 6319378907SAtari911 } 6419378907SAtari911 } 6519378907SAtari911 } 6619378907SAtari911 6719378907SAtari911 return $params; 6819378907SAtari911 } 6919378907SAtari911 7019378907SAtari911 public function render($mode, Doku_Renderer $renderer, $data) { 7119378907SAtari911 if ($mode !== 'xhtml') return false; 7219378907SAtari911 7319378907SAtari911 if ($data['type'] === 'eventlist') { 7419378907SAtari911 $html = $this->renderStandaloneEventList($data); 7519378907SAtari911 } elseif ($data['type'] === 'eventpanel') { 7619378907SAtari911 $html = $this->renderEventPanelOnly($data); 7719378907SAtari911 } else { 7819378907SAtari911 $html = $this->renderCompactCalendar($data); 7919378907SAtari911 } 8019378907SAtari911 8119378907SAtari911 $renderer->doc .= $html; 8219378907SAtari911 return true; 8319378907SAtari911 } 8419378907SAtari911 8519378907SAtari911 private function renderCompactCalendar($data) { 8619378907SAtari911 $year = (int)$data['year']; 8719378907SAtari911 $month = (int)$data['month']; 8819378907SAtari911 $namespace = $data['namespace']; 8919378907SAtari911 90*0c3b6e81SAtari911 // Get theme - prefer inline theme= parameter, fall back to admin default 91*0c3b6e81SAtari911 $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme(); 929ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 939ccd446eSAtari911 $themeClass = 'calendar-theme-' . $theme; 949ccd446eSAtari911 959ccd446eSAtari911 // Determine button text color: professional uses white, others use bg color 969ccd446eSAtari911 $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg']; 979ccd446eSAtari911 98e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 99e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 100e3a9f44cSAtari911 101e3a9f44cSAtari911 if ($isMultiNamespace) { 102e3a9f44cSAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month); 103e3a9f44cSAtari911 } else { 10419378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 105e3a9f44cSAtari911 } 10619378907SAtari911 $calId = 'cal_' . md5(serialize($data) . microtime()); 10719378907SAtari911 10819378907SAtari911 $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); 10919378907SAtari911 11019378907SAtari911 $prevMonth = $month - 1; 11119378907SAtari911 $prevYear = $year; 11219378907SAtari911 if ($prevMonth < 1) { 11319378907SAtari911 $prevMonth = 12; 11419378907SAtari911 $prevYear--; 11519378907SAtari911 } 11619378907SAtari911 11719378907SAtari911 $nextMonth = $month + 1; 11819378907SAtari911 $nextYear = $year; 11919378907SAtari911 if ($nextMonth > 12) { 12019378907SAtari911 $nextMonth = 1; 12119378907SAtari911 $nextYear++; 12219378907SAtari911 } 12319378907SAtari911 1249ccd446eSAtari911 // Container - all styling via CSS variables 1259ccd446eSAtari911 $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)) . '">'; 1269ccd446eSAtari911 1279ccd446eSAtari911 // Inject CSS variables for this calendar instance - all theming flows from here 1289ccd446eSAtari911 $html .= '<style> 1299ccd446eSAtari911 #' . $calId . ' { 1309ccd446eSAtari911 --background-site: ' . $themeStyles['bg'] . '; 1319ccd446eSAtari911 --background-alt: ' . $themeStyles['cell_bg'] . '; 1329ccd446eSAtari911 --background-header: ' . $themeStyles['header_bg'] . '; 1339ccd446eSAtari911 --text-primary: ' . $themeStyles['text_primary'] . '; 1349ccd446eSAtari911 --text-dim: ' . $themeStyles['text_dim'] . '; 1359ccd446eSAtari911 --text-bright: ' . $themeStyles['text_bright'] . '; 1369ccd446eSAtari911 --border-color: ' . $themeStyles['grid_border'] . '; 1379ccd446eSAtari911 --border-main: ' . $themeStyles['border'] . '; 1389ccd446eSAtari911 --cell-bg: ' . $themeStyles['cell_bg'] . '; 1399ccd446eSAtari911 --cell-today-bg: ' . $themeStyles['cell_today_bg'] . '; 1409ccd446eSAtari911 --shadow-color: ' . $themeStyles['shadow'] . '; 1419ccd446eSAtari911 --header-border: ' . $themeStyles['header_border'] . '; 1429ccd446eSAtari911 --header-shadow: ' . $themeStyles['header_shadow'] . '; 1439ccd446eSAtari911 --grid-bg: ' . $themeStyles['grid_bg'] . '; 1449ccd446eSAtari911 --btn-text: ' . $btnTextColor . '; 1459ccd446eSAtari911 } 1469ccd446eSAtari911 #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; } 1479ccd446eSAtari911 #event-search-' . $calId . '::-webkit-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; } 1489ccd446eSAtari911 #event-search-' . $calId . '::-moz-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; } 1499ccd446eSAtari911 #event-search-' . $calId . ':-ms-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; } 1509ccd446eSAtari911 </style>'; 1511d05cddcSAtari911 1521d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 1531d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 1541d05cddcSAtari911 1551d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 1561d05cddcSAtari911 $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>'; 15719378907SAtari911 15819378907SAtari911 // Embed events data as JSON for JavaScript access 15919378907SAtari911 $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>'; 16019378907SAtari911 16119378907SAtari911 // Left side: Calendar 16219378907SAtari911 $html .= '<div class="calendar-compact-left">'; 16319378907SAtari911 16419378907SAtari911 // Header with navigation 16519378907SAtari911 $html .= '<div class="calendar-compact-header">'; 16619378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 16787ac9bf3SAtari911 $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . '</h3>'; 16819378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 16987ac9bf3SAtari911 $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>'; 17019378907SAtari911 $html .= '</div>'; 17119378907SAtari911 172*0c3b6e81SAtari911 // Calendar grid - day name headers as a separate div (avoids Firefox th height issues) 173*0c3b6e81SAtari911 $html .= '<div class="calendar-day-headers">'; 174*0c3b6e81SAtari911 $html .= '<span>S</span><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span>'; 175*0c3b6e81SAtari911 $html .= '</div>'; 17619378907SAtari911 $html .= '<table class="calendar-compact-grid">'; 177*0c3b6e81SAtari911 $html .= '<tbody>'; 17819378907SAtari911 17919378907SAtari911 $firstDay = mktime(0, 0, 0, $month, 1, $year); 18019378907SAtari911 $daysInMonth = date('t', $firstDay); 18119378907SAtari911 $dayOfWeek = date('w', $firstDay); 18219378907SAtari911 183e3a9f44cSAtari911 // Build a map of all events with their date ranges for the calendar grid 18487ac9bf3SAtari911 $eventRanges = array(); 185e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 18687ac9bf3SAtari911 foreach ($dayEvents as $evt) { 18787ac9bf3SAtari911 $eventId = isset($evt['id']) ? $evt['id'] : ''; 18887ac9bf3SAtari911 $startDate = $dateKey; 18987ac9bf3SAtari911 $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey; 19087ac9bf3SAtari911 19187ac9bf3SAtari911 // Only process events that touch this month 19287ac9bf3SAtari911 $eventStart = new DateTime($startDate); 19387ac9bf3SAtari911 $eventEnd = new DateTime($endDate); 19487ac9bf3SAtari911 $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month)); 19587ac9bf3SAtari911 $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth)); 19687ac9bf3SAtari911 19787ac9bf3SAtari911 // Skip if event doesn't overlap with current month 19887ac9bf3SAtari911 if ($eventEnd < $monthStart || $eventStart > $monthEnd) { 19987ac9bf3SAtari911 continue; 20087ac9bf3SAtari911 } 20187ac9bf3SAtari911 20287ac9bf3SAtari911 // Create entry for each day the event spans 20387ac9bf3SAtari911 $current = clone $eventStart; 20487ac9bf3SAtari911 while ($current <= $eventEnd) { 20587ac9bf3SAtari911 $currentKey = $current->format('Y-m-d'); 20687ac9bf3SAtari911 20787ac9bf3SAtari911 // Check if this date is in current month 20887ac9bf3SAtari911 $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey); 20987ac9bf3SAtari911 if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) { 21087ac9bf3SAtari911 if (!isset($eventRanges[$currentKey])) { 21187ac9bf3SAtari911 $eventRanges[$currentKey] = array(); 21287ac9bf3SAtari911 } 21387ac9bf3SAtari911 21487ac9bf3SAtari911 // Add event with span information 21587ac9bf3SAtari911 $evt['_span_start'] = $startDate; 21687ac9bf3SAtari911 $evt['_span_end'] = $endDate; 21787ac9bf3SAtari911 $evt['_is_first_day'] = ($currentKey === $startDate); 21887ac9bf3SAtari911 $evt['_is_last_day'] = ($currentKey === $endDate); 21987ac9bf3SAtari911 $evt['_original_date'] = $dateKey; // Keep track of original date 22087ac9bf3SAtari911 22187ac9bf3SAtari911 // Check if event continues from previous month or to next month 22287ac9bf3SAtari911 $evt['_continues_from_prev'] = ($eventStart < $monthStart); 22387ac9bf3SAtari911 $evt['_continues_to_next'] = ($eventEnd > $monthEnd); 22487ac9bf3SAtari911 22587ac9bf3SAtari911 $eventRanges[$currentKey][] = $evt; 22687ac9bf3SAtari911 } 22787ac9bf3SAtari911 22887ac9bf3SAtari911 $current->modify('+1 day'); 22987ac9bf3SAtari911 } 23087ac9bf3SAtari911 } 23187ac9bf3SAtari911 } 23287ac9bf3SAtari911 23319378907SAtari911 $currentDay = 1; 23419378907SAtari911 $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7); 23519378907SAtari911 23619378907SAtari911 for ($row = 0; $row < $rowCount; $row++) { 23719378907SAtari911 $html .= '<tr>'; 23819378907SAtari911 for ($col = 0; $col < 7; $col++) { 23919378907SAtari911 if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) { 24019378907SAtari911 $html .= '<td class="cal-empty"></td>'; 24119378907SAtari911 } else { 24219378907SAtari911 $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay); 24319378907SAtari911 $isToday = ($dateKey === date('Y-m-d')); 24487ac9bf3SAtari911 $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]); 24519378907SAtari911 24619378907SAtari911 $classes = 'cal-day'; 24719378907SAtari911 if ($isToday) $classes .= ' cal-today'; 24819378907SAtari911 if ($hasEvents) $classes .= ' cal-has-events'; 24919378907SAtari911 25019378907SAtari911 $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">'; 2519ccd446eSAtari911 2529ccd446eSAtari911 $dayNumClass = $isToday ? 'day-num day-num-today' : 'day-num'; 2539ccd446eSAtari911 $html .= '<span class="' . $dayNumClass . '">' . $currentDay . '</span>'; 25419378907SAtari911 25519378907SAtari911 if ($hasEvents) { 25619378907SAtari911 // Sort events by time (no time first, then by time) 25787ac9bf3SAtari911 $sortedEvents = $eventRanges[$dateKey]; 25819378907SAtari911 usort($sortedEvents, function($a, $b) { 25919378907SAtari911 $timeA = isset($a['time']) ? $a['time'] : ''; 26019378907SAtari911 $timeB = isset($b['time']) ? $b['time'] : ''; 26119378907SAtari911 26219378907SAtari911 // Events without time go first 26319378907SAtari911 if (empty($timeA) && !empty($timeB)) return -1; 26419378907SAtari911 if (!empty($timeA) && empty($timeB)) return 1; 26519378907SAtari911 if (empty($timeA) && empty($timeB)) return 0; 26619378907SAtari911 26719378907SAtari911 // Sort by time 26819378907SAtari911 return strcmp($timeA, $timeB); 26919378907SAtari911 }); 27019378907SAtari911 27119378907SAtari911 // Show colored stacked bars for each event 27219378907SAtari911 $html .= '<div class="event-indicators">'; 27319378907SAtari911 foreach ($sortedEvents as $evt) { 27419378907SAtari911 $eventId = isset($evt['id']) ? $evt['id'] : ''; 27519378907SAtari911 $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db'; 27619378907SAtari911 $eventTime = isset($evt['time']) ? $evt['time'] : ''; 27719378907SAtari911 $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event'; 27887ac9bf3SAtari911 $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey; 27987ac9bf3SAtari911 $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true; 28087ac9bf3SAtari911 $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true; 28119378907SAtari911 28219378907SAtari911 $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed'; 28319378907SAtari911 28487ac9bf3SAtari911 // Add classes for multi-day spanning 28587ac9bf3SAtari911 if (!$isFirstDay) $barClass .= ' event-bar-continues'; 28687ac9bf3SAtari911 if (!$isLastDay) $barClass .= ' event-bar-continuing'; 28787ac9bf3SAtari911 28819378907SAtari911 $html .= '<span class="event-bar ' . $barClass . '" '; 28919378907SAtari911 $html .= 'style="background: ' . $eventColor . ';" '; 29019378907SAtari911 $html .= 'title="' . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" '; 29187ac9bf3SAtari911 $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">'; 29219378907SAtari911 $html .= '</span>'; 29319378907SAtari911 } 29419378907SAtari911 $html .= '</div>'; 29519378907SAtari911 } 29619378907SAtari911 29719378907SAtari911 $html .= '</td>'; 29819378907SAtari911 $currentDay++; 29919378907SAtari911 } 30019378907SAtari911 } 30119378907SAtari911 $html .= '</tr>'; 30219378907SAtari911 } 30319378907SAtari911 30419378907SAtari911 $html .= '</tbody></table>'; 30519378907SAtari911 $html .= '</div>'; // End calendar-left 30619378907SAtari911 30719378907SAtari911 // Right side: Event list 30819378907SAtari911 $html .= '<div class="calendar-compact-right">'; 30919378907SAtari911 $html .= '<div class="event-list-header">'; 31019378907SAtari911 $html .= '<div class="event-list-header-content">'; 31119378907SAtari911 $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>'; 31219378907SAtari911 if ($namespace) { 31319378907SAtari911 $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>'; 31419378907SAtari911 } 31519378907SAtari911 $html .= '</div>'; 3161d05cddcSAtari911 3171d05cddcSAtari911 // Search bar in header 3181d05cddcSAtari911 $html .= '<div class="event-search-container-inline">'; 3191d05cddcSAtari911 $html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" placeholder=" Search..." oninput="filterEvents(\'' . $calId . '\', this.value)">'; 3201d05cddcSAtari911 $html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>'; 3211d05cddcSAtari911 $html .= '</div>'; 3221d05cddcSAtari911 32319378907SAtari911 $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>'; 32419378907SAtari911 $html .= '</div>'; 32519378907SAtari911 32619378907SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">'; 3279ccd446eSAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace, $themeStyles); 32819378907SAtari911 $html .= '</div>'; 32919378907SAtari911 33019378907SAtari911 $html .= '</div>'; // End calendar-right 33119378907SAtari911 33219378907SAtari911 // Event dialog 333*0c3b6e81SAtari911 $html .= $this->renderEventDialog($calId, $namespace, $theme); 33419378907SAtari911 33587ac9bf3SAtari911 // Month/Year picker dialog (at container level for proper overlay) 3369ccd446eSAtari911 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles); 33787ac9bf3SAtari911 33819378907SAtari911 $html .= '</div>'; // End container 33919378907SAtari911 34019378907SAtari911 return $html; 34119378907SAtari911 } 34219378907SAtari911 3439ccd446eSAtari911 private function renderEventListContent($events, $calId, $namespace, $themeStyles = null) { 34419378907SAtari911 if (empty($events)) { 34519378907SAtari911 return '<p class="no-events-msg">No events this month</p>'; 34619378907SAtari911 } 34719378907SAtari911 3489ccd446eSAtari911 // Default theme styles if not provided 3499ccd446eSAtari911 if ($themeStyles === null) { 3509ccd446eSAtari911 $theme = $this->getSidebarTheme(); 3519ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 3529ccd446eSAtari911 } 3539ccd446eSAtari911 3541d05cddcSAtari911 // Check for time conflicts 3551d05cddcSAtari911 $events = $this->checkTimeConflicts($events); 3561d05cddcSAtari911 357e3a9f44cSAtari911 // Sort by date ascending (chronological order - oldest first) 35819378907SAtari911 ksort($events); 35919378907SAtari911 360e3a9f44cSAtari911 // Sort events within each day by time 361e3a9f44cSAtari911 foreach ($events as $dateKey => &$dayEvents) { 362e3a9f44cSAtari911 usort($dayEvents, function($a, $b) { 3631d05cddcSAtari911 $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null; 3641d05cddcSAtari911 $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null; 3651d05cddcSAtari911 3661d05cddcSAtari911 // All-day events (no time) go to the TOP 3671d05cddcSAtari911 if ($timeA === null && $timeB !== null) return -1; // A before B 3681d05cddcSAtari911 if ($timeA !== null && $timeB === null) return 1; // A after B 3691d05cddcSAtari911 if ($timeA === null && $timeB === null) return 0; // Both all-day, equal 3701d05cddcSAtari911 3711d05cddcSAtari911 // Both have times, sort chronologically 372e3a9f44cSAtari911 return strcmp($timeA, $timeB); 373e3a9f44cSAtari911 }); 374e3a9f44cSAtari911 } 375e3a9f44cSAtari911 unset($dayEvents); // Break reference 376e3a9f44cSAtari911 377e3a9f44cSAtari911 // Get today's date for comparison 378e3a9f44cSAtari911 $today = date('Y-m-d'); 379e3a9f44cSAtari911 $firstFutureEventId = null; 380e3a9f44cSAtari911 3811d05cddcSAtari911 // Helper function to check if event is past (with 15-minute grace period for timed events) 3821d05cddcSAtari911 $isEventPast = function($dateKey, $time) use ($today) { 3831d05cddcSAtari911 // If event is on a past date, it's definitely past 3841d05cddcSAtari911 if ($dateKey < $today) { 3851d05cddcSAtari911 return true; 3861d05cddcSAtari911 } 3871d05cddcSAtari911 3881d05cddcSAtari911 // If event is on a future date, it's definitely not past 3891d05cddcSAtari911 if ($dateKey > $today) { 3901d05cddcSAtari911 return false; 3911d05cddcSAtari911 } 3921d05cddcSAtari911 3931d05cddcSAtari911 // Event is today - check time with grace period 3941d05cddcSAtari911 if ($time && $time !== '') { 3951d05cddcSAtari911 try { 3961d05cddcSAtari911 $currentDateTime = new DateTime(); 3971d05cddcSAtari911 $eventDateTime = new DateTime($dateKey . ' ' . $time); 3981d05cddcSAtari911 3991d05cddcSAtari911 // Add 15-minute grace period 4001d05cddcSAtari911 $eventDateTime->modify('+15 minutes'); 4011d05cddcSAtari911 4021d05cddcSAtari911 // Event is past if current time > event time + 15 minutes 4031d05cddcSAtari911 return $currentDateTime > $eventDateTime; 4041d05cddcSAtari911 } catch (Exception $e) { 4051d05cddcSAtari911 // If time parsing fails, fall back to date-only comparison 4061d05cddcSAtari911 return false; 4071d05cddcSAtari911 } 4081d05cddcSAtari911 } 4091d05cddcSAtari911 4101d05cddcSAtari911 // No time specified for today's event, treat as future 4111d05cddcSAtari911 return false; 4121d05cddcSAtari911 }; 4131d05cddcSAtari911 4141d05cddcSAtari911 // Build HTML for each event - separate past/completed from future 4151d05cddcSAtari911 $pastHtml = ''; 4161d05cddcSAtari911 $futureHtml = ''; 4171d05cddcSAtari911 $pastCount = 0; 418e3a9f44cSAtari911 41919378907SAtari911 foreach ($events as $dateKey => $dayEvents) { 420e3a9f44cSAtari911 42119378907SAtari911 foreach ($dayEvents as $event) { 422e3a9f44cSAtari911 // Track first future/today event for auto-scroll 423e3a9f44cSAtari911 if (!$firstFutureEventId && $dateKey >= $today) { 424e3a9f44cSAtari911 $firstFutureEventId = isset($event['id']) ? $event['id'] : ''; 425e3a9f44cSAtari911 } 42619378907SAtari911 $eventId = isset($event['id']) ? $event['id'] : ''; 42719378907SAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 4281d05cddcSAtari911 $timeRaw = isset($event['time']) ? $event['time'] : ''; 4291d05cddcSAtari911 $time = htmlspecialchars($timeRaw); 4301d05cddcSAtari911 $endTime = isset($event['endTime']) ? htmlspecialchars($event['endTime']) : ''; 43119378907SAtari911 $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db'; 43219378907SAtari911 $description = isset($event['description']) ? $event['description'] : ''; 43319378907SAtari911 $isTask = isset($event['isTask']) ? $event['isTask'] : false; 43419378907SAtari911 $completed = isset($event['completed']) ? $event['completed'] : false; 43519378907SAtari911 $endDate = isset($event['endDate']) ? $event['endDate'] : ''; 43619378907SAtari911 4371d05cddcSAtari911 // Use helper function to determine if event is past (with grace period) 4381d05cddcSAtari911 $isPast = $isEventPast($dateKey, $timeRaw); 4391d05cddcSAtari911 $isToday = $dateKey === $today; 4401d05cddcSAtari911 4411d05cddcSAtari911 // Check if event should be in past section 4421d05cddcSAtari911 // EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past 4431d05cddcSAtari911 $isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed; 4441d05cddcSAtari911 if ($isPastOrCompleted) { 4451d05cddcSAtari911 $pastCount++; 4461d05cddcSAtari911 } 4471d05cddcSAtari911 4481d05cddcSAtari911 // Determine if task is past due (past date, is task, not completed) 4491d05cddcSAtari911 $isPastDue = $isPast && $isTask && !$completed; 4501d05cddcSAtari911 45119378907SAtari911 // Process description for wiki syntax, HTML, images, and links 4529ccd446eSAtari911 $renderedDescription = $this->renderDescription($description, $themeStyles); 45319378907SAtari911 4541d05cddcSAtari911 // Convert to 12-hour format and handle time ranges 45519378907SAtari911 $displayTime = ''; 45619378907SAtari911 if ($time) { 45719378907SAtari911 $timeObj = DateTime::createFromFormat('H:i', $time); 45819378907SAtari911 if ($timeObj) { 45919378907SAtari911 $displayTime = $timeObj->format('g:i A'); 4601d05cddcSAtari911 4611d05cddcSAtari911 // Add end time if present and different from start time 4621d05cddcSAtari911 if ($endTime && $endTime !== $time) { 4631d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $endTime); 4641d05cddcSAtari911 if ($endTimeObj) { 4651d05cddcSAtari911 $displayTime .= ' - ' . $endTimeObj->format('g:i A'); 4661d05cddcSAtari911 } 4671d05cddcSAtari911 } 46819378907SAtari911 } else { 46919378907SAtari911 $displayTime = $time; 47019378907SAtari911 } 47119378907SAtari911 } 47219378907SAtari911 47387ac9bf3SAtari911 // Format date display with day of week 474e3a9f44cSAtari911 // Use originalStartDate if this is a multi-month event continuation 475e3a9f44cSAtari911 $displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey; 476e3a9f44cSAtari911 $dateObj = new DateTime($displayDateKey); 47787ac9bf3SAtari911 $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24" 47819378907SAtari911 47919378907SAtari911 // Multi-day indicator 48019378907SAtari911 $multiDay = ''; 481e3a9f44cSAtari911 if ($endDate && $endDate !== $displayDateKey) { 48219378907SAtari911 $endObj = new DateTime($endDate); 48387ac9bf3SAtari911 $multiDay = ' → ' . $endObj->format('D, M j'); 48419378907SAtari911 } 48519378907SAtari911 48619378907SAtari911 $completedClass = $completed ? ' event-completed' : ''; 4871d05cddcSAtari911 // Don't grey out past due tasks - they need attention! 4881d05cddcSAtari911 $pastClass = ($isPast && !$isPastDue) ? ' event-past' : ''; 4891d05cddcSAtari911 $pastDueClass = $isPastDue ? ' event-pastdue' : ''; 490e3a9f44cSAtari911 $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : ''; 49119378907SAtari911 4929ccd446eSAtari911 // For all themes: use CSS variables, only keep border-left-color as inline 4939ccd446eSAtari911 $pastClickHandler = ($isPast && !$isPastDue) ? ' onclick="togglePastEventExpand(this)"' : ''; 4949ccd446eSAtari911 $eventHtml = '<div class="event-compact-item' . $completedClass . $pastClass . $pastDueClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ';"' . $pastClickHandler . $firstFutureAttr . '>'; 4951d05cddcSAtari911 $eventHtml .= '<div class="event-info">'; 4969ccd446eSAtari911 4971d05cddcSAtari911 $eventHtml .= '<div class="event-title-row">'; 4981d05cddcSAtari911 $eventHtml .= '<span class="event-title-compact">' . $title . '</span>'; 4991d05cddcSAtari911 $eventHtml .= '</div>'; 50019378907SAtari911 501e3a9f44cSAtari911 // For past events, hide meta and description (collapsed) 5021d05cddcSAtari911 // EXCEPTION: Past due tasks should show their details 5031d05cddcSAtari911 if (!$isPast || $isPastDue) { 5041d05cddcSAtari911 $eventHtml .= '<div class="event-meta-compact">'; 5051d05cddcSAtari911 $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay; 50619378907SAtari911 if ($displayTime) { 5071d05cddcSAtari911 $eventHtml .= ' • ' . $displayTime; 50819378907SAtari911 } 5091d05cddcSAtari911 // Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks 5101d05cddcSAtari911 if ($isPastDue) { 5111d05cddcSAtari911 $eventHtml .= ' <span class="event-pastdue-badge">PAST DUE</span>'; 5121d05cddcSAtari911 } elseif ($isToday) { 5131d05cddcSAtari911 $eventHtml .= ' <span class="event-today-badge">TODAY</span>'; 514e3a9f44cSAtari911 } 5151d05cddcSAtari911 // Add namespace badge - ALWAYS show if event has a namespace 516e3a9f44cSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 517e3a9f44cSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 518e3a9f44cSAtari911 $eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility 519e3a9f44cSAtari911 } 5201d05cddcSAtari911 // Show badge if namespace exists and is not empty 5211d05cddcSAtari911 if ($eventNamespace && $eventNamespace !== '') { 5221d05cddcSAtari911 $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>'; 523e3a9f44cSAtari911 } 5241d05cddcSAtari911 5251d05cddcSAtari911 // Add conflict warning if event has time conflicts 5261d05cddcSAtari911 if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) { 5271d05cddcSAtari911 $conflictList = []; 5281d05cddcSAtari911 foreach ($event['conflictsWith'] as $conflict) { 5299ccd446eSAtari911 $conflictText = $conflict['title']; 5301d05cddcSAtari911 if (!empty($conflict['time'])) { 5311d05cddcSAtari911 // Format time range 5321d05cddcSAtari911 $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']); 5331d05cddcSAtari911 $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time']; 5341d05cddcSAtari911 5351d05cddcSAtari911 if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) { 5361d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']); 5371d05cddcSAtari911 $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime']; 5381d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')'; 5391d05cddcSAtari911 } else { 5401d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ')'; 5411d05cddcSAtari911 } 5421d05cddcSAtari911 } 5431d05cddcSAtari911 $conflictList[] = $conflictText; 5441d05cddcSAtari911 } 5451d05cddcSAtari911 $conflictCount = count($event['conflictsWith']); 5469ccd446eSAtari911 $conflictJson = base64_encode(json_encode($conflictList)); 5471d05cddcSAtari911 $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>'; 5481d05cddcSAtari911 } 5491d05cddcSAtari911 5501d05cddcSAtari911 $eventHtml .= '</span>'; 5511d05cddcSAtari911 $eventHtml .= '</div>'; 55219378907SAtari911 55319378907SAtari911 if ($description) { 5541d05cddcSAtari911 $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>'; 5551d05cddcSAtari911 } 5561d05cddcSAtari911 } else { 5571d05cddcSAtari911 // Past events: render with display:none for click-to-expand 5581d05cddcSAtari911 $eventHtml .= '<div class="event-meta-compact" style="display:none;">'; 5591d05cddcSAtari911 $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay; 5601d05cddcSAtari911 if ($displayTime) { 5611d05cddcSAtari911 $eventHtml .= ' • ' . $displayTime; 5621d05cddcSAtari911 } 5631d05cddcSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 5641d05cddcSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 5651d05cddcSAtari911 $eventNamespace = $event['_namespace']; 5661d05cddcSAtari911 } 5671d05cddcSAtari911 if ($eventNamespace && $eventNamespace !== '') { 5681d05cddcSAtari911 $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>'; 5691d05cddcSAtari911 } 5701d05cddcSAtari911 5711d05cddcSAtari911 // Add conflict warning if event has time conflicts 5721d05cddcSAtari911 if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) { 5731d05cddcSAtari911 $conflictList = []; 5741d05cddcSAtari911 foreach ($event['conflictsWith'] as $conflict) { 5759ccd446eSAtari911 $conflictText = $conflict['title']; 5761d05cddcSAtari911 if (!empty($conflict['time'])) { 5771d05cddcSAtari911 $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']); 5781d05cddcSAtari911 $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time']; 5791d05cddcSAtari911 5801d05cddcSAtari911 if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) { 5811d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']); 5821d05cddcSAtari911 $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime']; 5831d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')'; 5841d05cddcSAtari911 } else { 5851d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ')'; 5861d05cddcSAtari911 } 5871d05cddcSAtari911 } 5881d05cddcSAtari911 $conflictList[] = $conflictText; 5891d05cddcSAtari911 } 5901d05cddcSAtari911 $conflictCount = count($event['conflictsWith']); 5919ccd446eSAtari911 $conflictJson = base64_encode(json_encode($conflictList)); 5921d05cddcSAtari911 $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>'; 5931d05cddcSAtari911 } 5941d05cddcSAtari911 5951d05cddcSAtari911 $eventHtml .= '</span>'; 5961d05cddcSAtari911 $eventHtml .= '</div>'; 5971d05cddcSAtari911 5981d05cddcSAtari911 if ($description) { 5991d05cddcSAtari911 $eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '</div>'; 60019378907SAtari911 } 601e3a9f44cSAtari911 } 60219378907SAtari911 6031d05cddcSAtari911 $eventHtml .= '</div>'; // event-info 60419378907SAtari911 605e3a9f44cSAtari911 // Use stored namespace from event, fallback to passed namespace 606e3a9f44cSAtari911 $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace; 607e3a9f44cSAtari911 6081d05cddcSAtari911 $eventHtml .= '<div class="event-actions-compact">'; 6091d05cddcSAtari911 $eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">️</button>'; 6101d05cddcSAtari911 $eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>'; 6111d05cddcSAtari911 $eventHtml .= '</div>'; 61219378907SAtari911 61319378907SAtari911 // Checkbox for tasks - ON THE FAR RIGHT 61419378907SAtari911 if ($isTask) { 61519378907SAtari911 $checked = $completed ? 'checked' : ''; 6161d05cddcSAtari911 $eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">'; 61719378907SAtari911 } 61819378907SAtari911 6191d05cddcSAtari911 $eventHtml .= '</div>'; 6201d05cddcSAtari911 6211d05cddcSAtari911 // Add to appropriate section 6221d05cddcSAtari911 if ($isPastOrCompleted) { 6231d05cddcSAtari911 $pastHtml .= $eventHtml; 6241d05cddcSAtari911 } else { 6251d05cddcSAtari911 $futureHtml .= $eventHtml; 6261d05cddcSAtari911 } 6271d05cddcSAtari911 } 6281d05cddcSAtari911 } 6291d05cddcSAtari911 6301d05cddcSAtari911 // Build final HTML with collapsible past events section 6311d05cddcSAtari911 $html = ''; 6321d05cddcSAtari911 6331d05cddcSAtari911 // Add collapsible past events section if any exist 6341d05cddcSAtari911 if ($pastCount > 0) { 6351d05cddcSAtari911 $html .= '<div class="past-events-section">'; 6361d05cddcSAtari911 $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">'; 6371d05cddcSAtari911 $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> '; 6381d05cddcSAtari911 $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>'; 63919378907SAtari911 $html .= '</div>'; 6401d05cddcSAtari911 $html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">'; 6411d05cddcSAtari911 $html .= $pastHtml; 6421d05cddcSAtari911 $html .= '</div>'; 6431d05cddcSAtari911 $html .= '</div>'; 6441d05cddcSAtari911 } 645e3a9f44cSAtari911 6461d05cddcSAtari911 // Add future events 6471d05cddcSAtari911 $html .= $futureHtml; 64819378907SAtari911 64919378907SAtari911 return $html; 65019378907SAtari911 } 65119378907SAtari911 6521d05cddcSAtari911 /** 6531d05cddcSAtari911 * Check for time conflicts between events 6541d05cddcSAtari911 */ 6551d05cddcSAtari911 private function checkTimeConflicts($events) { 6561d05cddcSAtari911 // Group events by date 6571d05cddcSAtari911 $eventsByDate = []; 6581d05cddcSAtari911 foreach ($events as $date => $dateEvents) { 6591d05cddcSAtari911 if (!is_array($dateEvents)) continue; 6601d05cddcSAtari911 6611d05cddcSAtari911 foreach ($dateEvents as $evt) { 6621d05cddcSAtari911 if (empty($evt['time'])) continue; // Skip all-day events 6631d05cddcSAtari911 6641d05cddcSAtari911 if (!isset($eventsByDate[$date])) { 6651d05cddcSAtari911 $eventsByDate[$date] = []; 6661d05cddcSAtari911 } 6671d05cddcSAtari911 $eventsByDate[$date][] = $evt; 6681d05cddcSAtari911 } 6691d05cddcSAtari911 } 6701d05cddcSAtari911 6711d05cddcSAtari911 // Check for overlaps on each date 6721d05cddcSAtari911 foreach ($eventsByDate as $date => $dateEvents) { 6731d05cddcSAtari911 for ($i = 0; $i < count($dateEvents); $i++) { 6741d05cddcSAtari911 for ($j = $i + 1; $j < count($dateEvents); $j++) { 6751d05cddcSAtari911 if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) { 6761d05cddcSAtari911 // Mark both events as conflicting 6771d05cddcSAtari911 $dateEvents[$i]['hasConflict'] = true; 6781d05cddcSAtari911 $dateEvents[$j]['hasConflict'] = true; 6791d05cddcSAtari911 6801d05cddcSAtari911 // Store conflict info 6811d05cddcSAtari911 if (!isset($dateEvents[$i]['conflictsWith'])) { 6821d05cddcSAtari911 $dateEvents[$i]['conflictsWith'] = []; 6831d05cddcSAtari911 } 6841d05cddcSAtari911 if (!isset($dateEvents[$j]['conflictsWith'])) { 6851d05cddcSAtari911 $dateEvents[$j]['conflictsWith'] = []; 6861d05cddcSAtari911 } 6871d05cddcSAtari911 6881d05cddcSAtari911 $dateEvents[$i]['conflictsWith'][] = [ 6891d05cddcSAtari911 'id' => $dateEvents[$j]['id'], 6901d05cddcSAtari911 'title' => $dateEvents[$j]['title'], 6911d05cddcSAtari911 'time' => $dateEvents[$j]['time'], 6921d05cddcSAtari911 'endTime' => isset($dateEvents[$j]['endTime']) ? $dateEvents[$j]['endTime'] : '' 6931d05cddcSAtari911 ]; 6941d05cddcSAtari911 6951d05cddcSAtari911 $dateEvents[$j]['conflictsWith'][] = [ 6961d05cddcSAtari911 'id' => $dateEvents[$i]['id'], 6971d05cddcSAtari911 'title' => $dateEvents[$i]['title'], 6981d05cddcSAtari911 'time' => $dateEvents[$i]['time'], 6991d05cddcSAtari911 'endTime' => isset($dateEvents[$i]['endTime']) ? $dateEvents[$i]['endTime'] : '' 7001d05cddcSAtari911 ]; 7011d05cddcSAtari911 } 7021d05cddcSAtari911 } 7031d05cddcSAtari911 } 7041d05cddcSAtari911 7051d05cddcSAtari911 // Update the events array with conflict information 7061d05cddcSAtari911 foreach ($events[$date] as &$evt) { 7071d05cddcSAtari911 foreach ($dateEvents as $checkedEvt) { 7081d05cddcSAtari911 if ($evt['id'] === $checkedEvt['id']) { 7091d05cddcSAtari911 if (isset($checkedEvt['hasConflict'])) { 7101d05cddcSAtari911 $evt['hasConflict'] = $checkedEvt['hasConflict']; 7111d05cddcSAtari911 } 7121d05cddcSAtari911 if (isset($checkedEvt['conflictsWith'])) { 7131d05cddcSAtari911 $evt['conflictsWith'] = $checkedEvt['conflictsWith']; 7141d05cddcSAtari911 } 7151d05cddcSAtari911 break; 7161d05cddcSAtari911 } 7171d05cddcSAtari911 } 7181d05cddcSAtari911 } 7191d05cddcSAtari911 } 7201d05cddcSAtari911 7211d05cddcSAtari911 return $events; 7221d05cddcSAtari911 } 7231d05cddcSAtari911 7241d05cddcSAtari911 /** 7251d05cddcSAtari911 * Check if two events overlap in time 7261d05cddcSAtari911 */ 7271d05cddcSAtari911 private function eventsOverlap($evt1, $evt2) { 7281d05cddcSAtari911 if (empty($evt1['time']) || empty($evt2['time'])) { 7291d05cddcSAtari911 return false; // All-day events don't conflict 7301d05cddcSAtari911 } 7311d05cddcSAtari911 7321d05cddcSAtari911 $start1 = $evt1['time']; 7331d05cddcSAtari911 $end1 = isset($evt1['endTime']) && !empty($evt1['endTime']) ? $evt1['endTime'] : $evt1['time']; 7341d05cddcSAtari911 7351d05cddcSAtari911 $start2 = $evt2['time']; 7361d05cddcSAtari911 $end2 = isset($evt2['endTime']) && !empty($evt2['endTime']) ? $evt2['endTime'] : $evt2['time']; 7371d05cddcSAtari911 7381d05cddcSAtari911 // Convert to minutes for easier comparison 7391d05cddcSAtari911 $start1Mins = $this->timeToMinutes($start1); 7401d05cddcSAtari911 $end1Mins = $this->timeToMinutes($end1); 7411d05cddcSAtari911 $start2Mins = $this->timeToMinutes($start2); 7421d05cddcSAtari911 $end2Mins = $this->timeToMinutes($end2); 7431d05cddcSAtari911 7441d05cddcSAtari911 // Check for overlap: start1 < end2 AND start2 < end1 7451d05cddcSAtari911 return $start1Mins < $end2Mins && $start2Mins < $end1Mins; 7461d05cddcSAtari911 } 7471d05cddcSAtari911 7481d05cddcSAtari911 /** 7491d05cddcSAtari911 * Convert HH:MM time to minutes since midnight 7501d05cddcSAtari911 */ 7511d05cddcSAtari911 private function timeToMinutes($timeStr) { 7521d05cddcSAtari911 $parts = explode(':', $timeStr); 7531d05cddcSAtari911 if (count($parts) !== 2) return 0; 7541d05cddcSAtari911 7551d05cddcSAtari911 return (int)$parts[0] * 60 + (int)$parts[1]; 7561d05cddcSAtari911 } 7571d05cddcSAtari911 75819378907SAtari911 private function renderEventPanelOnly($data) { 75919378907SAtari911 $year = (int)$data['year']; 76019378907SAtari911 $month = (int)$data['month']; 76119378907SAtari911 $namespace = $data['namespace']; 76287ac9bf3SAtari911 $height = isset($data['height']) ? $data['height'] : '400px'; 76387ac9bf3SAtari911 76487ac9bf3SAtari911 // Validate height format (must be px, em, rem, vh, or %) 76587ac9bf3SAtari911 if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) { 76687ac9bf3SAtari911 $height = '400px'; // Default fallback 76787ac9bf3SAtari911 } 76819378907SAtari911 769*0c3b6e81SAtari911 // Get theme - prefer inline theme= parameter, fall back to admin default 770*0c3b6e81SAtari911 $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme(); $themeStyles = $this->getSidebarThemeStyles($theme); 7719ccd446eSAtari911 772e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 773e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 774e3a9f44cSAtari911 775e3a9f44cSAtari911 if ($isMultiNamespace) { 776e3a9f44cSAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month); 777e3a9f44cSAtari911 } else { 77819378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 779e3a9f44cSAtari911 } 78019378907SAtari911 $calId = 'panel_' . md5(serialize($data) . microtime()); 78119378907SAtari911 78219378907SAtari911 $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); 78319378907SAtari911 78419378907SAtari911 $prevMonth = $month - 1; 78519378907SAtari911 $prevYear = $year; 78619378907SAtari911 if ($prevMonth < 1) { 78719378907SAtari911 $prevMonth = 12; 78819378907SAtari911 $prevYear--; 78919378907SAtari911 } 79019378907SAtari911 79119378907SAtari911 $nextMonth = $month + 1; 79219378907SAtari911 $nextYear = $year; 79319378907SAtari911 if ($nextMonth > 12) { 79419378907SAtari911 $nextMonth = 1; 79519378907SAtari911 $nextYear++; 79619378907SAtari911 } 79719378907SAtari911 7989ccd446eSAtari911 // Determine button text color based on theme 7999ccd446eSAtari911 $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg']; 8009ccd446eSAtari911 8019ccd446eSAtari911 $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)) . '">'; 8029ccd446eSAtari911 8039ccd446eSAtari911 // Inject CSS variables for this panel instance - same as main calendar 8049ccd446eSAtari911 $html .= '<style> 8059ccd446eSAtari911 #' . $calId . ' { 8069ccd446eSAtari911 --background-site: ' . $themeStyles['bg'] . '; 8079ccd446eSAtari911 --background-alt: ' . $themeStyles['cell_bg'] . '; 8089ccd446eSAtari911 --background-header: ' . $themeStyles['header_bg'] . '; 8099ccd446eSAtari911 --text-primary: ' . $themeStyles['text_primary'] . '; 8109ccd446eSAtari911 --text-dim: ' . $themeStyles['text_dim'] . '; 8119ccd446eSAtari911 --text-bright: ' . $themeStyles['text_bright'] . '; 8129ccd446eSAtari911 --border-color: ' . $themeStyles['grid_border'] . '; 8139ccd446eSAtari911 --border-main: ' . $themeStyles['border'] . '; 8149ccd446eSAtari911 --cell-bg: ' . $themeStyles['cell_bg'] . '; 8159ccd446eSAtari911 --cell-today-bg: ' . $themeStyles['cell_today_bg'] . '; 8169ccd446eSAtari911 --shadow-color: ' . $themeStyles['shadow'] . '; 8179ccd446eSAtari911 --header-border: ' . $themeStyles['header_border'] . '; 8189ccd446eSAtari911 --header-shadow: ' . $themeStyles['header_shadow'] . '; 8199ccd446eSAtari911 --grid-bg: ' . $themeStyles['grid_bg'] . '; 8209ccd446eSAtari911 --btn-text: ' . $btnTextColor . '; 8219ccd446eSAtari911 } 8229ccd446eSAtari911 #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; } 8239ccd446eSAtari911 </style>'; 82419378907SAtari911 8251d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 8261d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 8271d05cddcSAtari911 8281d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 8291d05cddcSAtari911 $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>'; 8301d05cddcSAtari911 8311d05cddcSAtari911 // Compact two-row header designed for ~500px width 8321d05cddcSAtari911 $html .= '<div class="panel-header-compact">'; 8331d05cddcSAtari911 8341d05cddcSAtari911 // Row 1: Navigation and title 8351d05cddcSAtari911 $html .= '<div class="panel-header-row-1">'; 8361d05cddcSAtari911 $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 8371d05cddcSAtari911 8381d05cddcSAtari911 // Compact month name (e.g. "Feb 2026" instead of "February 2026 Events") 8391d05cddcSAtari911 $shortMonthName = date('M Y', mktime(0, 0, 0, $month, 1, $year)); 8401d05cddcSAtari911 $html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $shortMonthName . '</h3>'; 8411d05cddcSAtari911 8421d05cddcSAtari911 $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 8431d05cddcSAtari911 8441d05cddcSAtari911 // Namespace badge (if applicable) 84587ac9bf3SAtari911 if ($namespace) { 846e3a9f44cSAtari911 if ($isMultiNamespace) { 847e3a9f44cSAtari911 if (strpos($namespace, '*') !== false) { 8481d05cddcSAtari911 $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>'; 849e3a9f44cSAtari911 } else { 850e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespace)); 8511d05cddcSAtari911 $nsCount = count($namespaceList); 8521d05cddcSAtari911 $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars(implode(', ', $namespaceList)) . '">' . $nsCount . ' NS</span>'; 853e3a9f44cSAtari911 } 854e3a9f44cSAtari911 } else { 8551d05cddcSAtari911 $isFiltering = ($namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false); 8561d05cddcSAtari911 if ($isFiltering) { 8571d05cddcSAtari911 $html .= '<span class="panel-ns-badge filter-on" title="Filtering by ' . htmlspecialchars($namespace) . ' - click to clear" onclick="clearNamespaceFilterPanel(\'' . $calId . '\')">' . htmlspecialchars($namespace) . ' ✕</span>'; 8581d05cddcSAtari911 } else { 8591d05cddcSAtari911 $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>'; 86087ac9bf3SAtari911 } 861e3a9f44cSAtari911 } 8621d05cddcSAtari911 } 8631d05cddcSAtari911 8641d05cddcSAtari911 $html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>'; 86519378907SAtari911 $html .= '</div>'; 86619378907SAtari911 8671d05cddcSAtari911 // Row 2: Search and add button 8681d05cddcSAtari911 $html .= '<div class="panel-header-row-2">'; 8691d05cddcSAtari911 $html .= '<div class="panel-search-box">'; 8701d05cddcSAtari911 $html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placeholder="Search events..." oninput="filterEvents(\'' . $calId . '\', this.value)">'; 8711d05cddcSAtari911 $html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>'; 8721d05cddcSAtari911 $html .= '</div>'; 8731d05cddcSAtari911 $html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>'; 8741d05cddcSAtari911 $html .= '</div>'; 8751d05cddcSAtari911 87619378907SAtari911 $html .= '</div>'; 87719378907SAtari911 87887ac9bf3SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">'; 87919378907SAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace); 88019378907SAtari911 $html .= '</div>'; 88119378907SAtari911 882*0c3b6e81SAtari911 $html .= $this->renderEventDialog($calId, $namespace, $theme); 88319378907SAtari911 88487ac9bf3SAtari911 // Month/Year picker for event panel 8859ccd446eSAtari911 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles); 88687ac9bf3SAtari911 88719378907SAtari911 $html .= '</div>'; 88819378907SAtari911 88919378907SAtari911 return $html; 89019378907SAtari911 } 89119378907SAtari911 89219378907SAtari911 private function renderStandaloneEventList($data) { 89319378907SAtari911 $namespace = $data['namespace']; 8941d05cddcSAtari911 // If no namespace specified, show all namespaces 8951d05cddcSAtari911 if (empty($namespace)) { 8961d05cddcSAtari911 $namespace = '*'; 8971d05cddcSAtari911 } 89819378907SAtari911 $daterange = $data['daterange']; 89919378907SAtari911 $date = $data['date']; 900e3a9f44cSAtari911 $range = isset($data['range']) ? strtolower($data['range']) : ''; 90187ac9bf3SAtari911 $today = isset($data['today']) ? true : false; 902e3a9f44cSAtari911 $sidebar = isset($data['sidebar']) ? true : false; 9031d05cddcSAtari911 $showchecked = isset($data['showchecked']) ? true : false; // New parameter 9041d05cddcSAtari911 $noheader = isset($data['noheader']) ? true : false; // New parameter to hide header 90519378907SAtari911 906e3a9f44cSAtari911 // Handle "range" parameter - day, week, or month 907e3a9f44cSAtari911 if ($range === 'day') { 9081d05cddcSAtari911 $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks 90987ac9bf3SAtari911 $endDate = date('Y-m-d'); 910e3a9f44cSAtari911 $headerText = 'Today'; 911e3a9f44cSAtari911 } elseif ($range === 'week') { 9121d05cddcSAtari911 $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks 9131d05cddcSAtari911 $endDateTime = new DateTime(); 914e3a9f44cSAtari911 $endDateTime->modify('+7 days'); 915e3a9f44cSAtari911 $endDate = $endDateTime->format('Y-m-d'); 916e3a9f44cSAtari911 $headerText = 'This Week'; 917e3a9f44cSAtari911 } elseif ($range === 'month') { 9181d05cddcSAtari911 $startDate = date('Y-m-01', strtotime('-1 month')); // Include previous month for past due tasks 919e3a9f44cSAtari911 $endDate = date('Y-m-t'); // Last of current month 9201d05cddcSAtari911 $dt = new DateTime(); 921e3a9f44cSAtari911 $headerText = $dt->format('F Y'); 922e3a9f44cSAtari911 } elseif ($sidebar) { 9231d05cddcSAtari911 // NEW: Sidebar widget - load current week's events 9249ccd446eSAtari911 $weekStartDay = $this->getWeekStartDay(); // Get saved preference 9259ccd446eSAtari911 9269ccd446eSAtari911 if ($weekStartDay === 'monday') { 9279ccd446eSAtari911 // Monday start 9281d05cddcSAtari911 $weekStart = date('Y-m-d', strtotime('monday this week')); 9291d05cddcSAtari911 $weekEnd = date('Y-m-d', strtotime('sunday this week')); 9309ccd446eSAtari911 } else { 9319ccd446eSAtari911 // Sunday start (default - US/Canada standard) 9329ccd446eSAtari911 $today = date('w'); // 0 (Sun) to 6 (Sat) 9339ccd446eSAtari911 if ($today == 0) { 9349ccd446eSAtari911 // Today is Sunday 9359ccd446eSAtari911 $weekStart = date('Y-m-d'); 9369ccd446eSAtari911 } else { 9379ccd446eSAtari911 // Monday-Saturday: go back to last Sunday 9389ccd446eSAtari911 $weekStart = date('Y-m-d', strtotime('-' . $today . ' days')); 9399ccd446eSAtari911 } 9409ccd446eSAtari911 $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days')); 9419ccd446eSAtari911 } 9421d05cddcSAtari911 9439ccd446eSAtari911 // Load events for the entire week PLUS tomorrow (if tomorrow is outside week) 9449ccd446eSAtari911 // PLUS next 2 weeks for Important events 9451d05cddcSAtari911 $start = new DateTime($weekStart); 9461d05cddcSAtari911 $end = new DateTime($weekEnd); 9479ccd446eSAtari911 9489ccd446eSAtari911 // Check if we need to extend to include tomorrow 9499ccd446eSAtari911 $tomorrowDate = date('Y-m-d', strtotime('+1 day')); 9509ccd446eSAtari911 if ($tomorrowDate > $weekEnd) { 9519ccd446eSAtari911 // Tomorrow is outside the week, extend end date to include it 9529ccd446eSAtari911 $end = new DateTime($tomorrowDate); 9539ccd446eSAtari911 } 9549ccd446eSAtari911 9559ccd446eSAtari911 // Extend 2 weeks into the future for Important events 9569ccd446eSAtari911 $twoWeeksOut = date('Y-m-d', strtotime($weekEnd . ' +14 days')); 9579ccd446eSAtari911 $end = new DateTime($twoWeeksOut); 9589ccd446eSAtari911 9591d05cddcSAtari911 $end->modify('+1 day'); // DatePeriod excludes end date 9601d05cddcSAtari911 $interval = new DateInterval('P1D'); 9611d05cddcSAtari911 $period = new DatePeriod($start, $interval, $end); 9621d05cddcSAtari911 9631d05cddcSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 9641d05cddcSAtari911 $allEvents = []; 9651d05cddcSAtari911 $loadedMonths = []; 9661d05cddcSAtari911 9671d05cddcSAtari911 foreach ($period as $dt) { 9681d05cddcSAtari911 $year = (int)$dt->format('Y'); 9691d05cddcSAtari911 $month = (int)$dt->format('n'); 9701d05cddcSAtari911 $dateKey = $dt->format('Y-m-d'); 9711d05cddcSAtari911 9721d05cddcSAtari911 $monthKey = $year . '-' . $month . '-' . $namespace; 9731d05cddcSAtari911 9741d05cddcSAtari911 if (!isset($loadedMonths[$monthKey])) { 9751d05cddcSAtari911 if ($isMultiNamespace) { 9761d05cddcSAtari911 $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month); 9771d05cddcSAtari911 } else { 9781d05cddcSAtari911 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month); 9791d05cddcSAtari911 } 9801d05cddcSAtari911 } 9811d05cddcSAtari911 9821d05cddcSAtari911 $monthEvents = $loadedMonths[$monthKey]; 9831d05cddcSAtari911 9841d05cddcSAtari911 if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) { 9851d05cddcSAtari911 $allEvents[$dateKey] = $monthEvents[$dateKey]; 9861d05cddcSAtari911 } 9871d05cddcSAtari911 } 9881d05cddcSAtari911 9891d05cddcSAtari911 // Apply time conflict detection 9901d05cddcSAtari911 $allEvents = $this->checkTimeConflicts($allEvents); 9911d05cddcSAtari911 9921d05cddcSAtari911 $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8); 9931d05cddcSAtari911 9941d05cddcSAtari911 // Render sidebar widget and return immediately 995*0c3b6e81SAtari911 $themeOverride = !empty($data['theme']) ? $data['theme'] : null; 996*0c3b6e81SAtari911 return $this->renderSidebarWidget($allEvents, $namespace, $calId, $themeOverride); 997e3a9f44cSAtari911 } elseif ($today) { 998e3a9f44cSAtari911 $startDate = date('Y-m-d'); 999e3a9f44cSAtari911 $endDate = date('Y-m-d'); 1000e3a9f44cSAtari911 $headerText = 'Today'; 100187ac9bf3SAtari911 } elseif ($daterange) { 100219378907SAtari911 list($startDate, $endDate) = explode(':', $daterange); 1003e3a9f44cSAtari911 $start = new DateTime($startDate); 1004e3a9f44cSAtari911 $end = new DateTime($endDate); 1005e3a9f44cSAtari911 $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y'); 100619378907SAtari911 } elseif ($date) { 100719378907SAtari911 $startDate = $date; 100819378907SAtari911 $endDate = $date; 1009e3a9f44cSAtari911 $dt = new DateTime($date); 1010e3a9f44cSAtari911 $headerText = $dt->format('l, F j, Y'); 101119378907SAtari911 } else { 101219378907SAtari911 $startDate = date('Y-m-01'); 101319378907SAtari911 $endDate = date('Y-m-t'); 1014e3a9f44cSAtari911 $dt = new DateTime($startDate); 1015e3a9f44cSAtari911 $headerText = $dt->format('F Y'); 101619378907SAtari911 } 101719378907SAtari911 1018e3a9f44cSAtari911 // Load all events in date range 101919378907SAtari911 $allEvents = array(); 102019378907SAtari911 $start = new DateTime($startDate); 102119378907SAtari911 $end = new DateTime($endDate); 102219378907SAtari911 $end->modify('+1 day'); 102319378907SAtari911 102419378907SAtari911 $interval = new DateInterval('P1D'); 102519378907SAtari911 $period = new DatePeriod($start, $interval, $end); 102619378907SAtari911 1027e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 1028e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 1029e3a9f44cSAtari911 103019378907SAtari911 static $loadedMonths = array(); 103119378907SAtari911 103219378907SAtari911 foreach ($period as $dt) { 103319378907SAtari911 $year = (int)$dt->format('Y'); 103419378907SAtari911 $month = (int)$dt->format('n'); 103519378907SAtari911 $dateKey = $dt->format('Y-m-d'); 103619378907SAtari911 1037e3a9f44cSAtari911 $monthKey = $year . '-' . $month . '-' . $namespace; 103819378907SAtari911 103919378907SAtari911 if (!isset($loadedMonths[$monthKey])) { 1040e3a9f44cSAtari911 if ($isMultiNamespace) { 1041e3a9f44cSAtari911 $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month); 1042e3a9f44cSAtari911 } else { 104319378907SAtari911 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month); 104419378907SAtari911 } 1045e3a9f44cSAtari911 } 104619378907SAtari911 104719378907SAtari911 $monthEvents = $loadedMonths[$monthKey]; 104819378907SAtari911 104919378907SAtari911 if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) { 105019378907SAtari911 $allEvents[$dateKey] = $monthEvents[$dateKey]; 105119378907SAtari911 } 105219378907SAtari911 } 105319378907SAtari911 10541d05cddcSAtari911 // Sort events by date (already sorted by dateKey), then by time within each day 10551d05cddcSAtari911 foreach ($allEvents as $dateKey => &$dayEvents) { 10561d05cddcSAtari911 usort($dayEvents, function($a, $b) { 10571d05cddcSAtari911 $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null; 10581d05cddcSAtari911 $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null; 10591d05cddcSAtari911 10601d05cddcSAtari911 // All-day events (no time) go to the TOP 10611d05cddcSAtari911 if ($timeA === null && $timeB !== null) return -1; // A before B 10621d05cddcSAtari911 if ($timeA !== null && $timeB === null) return 1; // A after B 10631d05cddcSAtari911 if ($timeA === null && $timeB === null) return 0; // Both all-day, equal 10641d05cddcSAtari911 10651d05cddcSAtari911 // Both have times, sort chronologically 10661d05cddcSAtari911 return strcmp($timeA, $timeB); 10671d05cddcSAtari911 }); 10681d05cddcSAtari911 } 10691d05cddcSAtari911 unset($dayEvents); // Break reference 10701d05cddcSAtari911 1071e3a9f44cSAtari911 // Simple 2-line display widget 10721d05cddcSAtari911 $calId = 'eventlist_' . uniqid(); 10731d05cddcSAtari911 $html = '<div class="eventlist-simple" id="' . $calId . '">'; 10741d05cddcSAtari911 10751d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 10761d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 10771d05cddcSAtari911 10781d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 10791d05cddcSAtari911 $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>'; 10801d05cddcSAtari911 10811d05cddcSAtari911 // Add compact header with date and clock for "today" mode (unless noheader is set) 10821d05cddcSAtari911 if ($today && !empty($allEvents) && !$noheader) { 10831d05cddcSAtari911 $todayDate = new DateTime(); 10841d05cddcSAtari911 $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026" 10851d05cddcSAtari911 $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM" 10861d05cddcSAtari911 10871d05cddcSAtari911 $html .= '<div class="eventlist-today-header">'; 10881d05cddcSAtari911 $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>'; 10891d05cddcSAtari911 $html .= '<div class="eventlist-bottom-info">'; 10901d05cddcSAtari911 $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">️</span> <span id="weather-temp-' . $calId . '">--°</span></span>'; 10911d05cddcSAtari911 $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>'; 10921d05cddcSAtari911 $html .= '</div>'; 10931d05cddcSAtari911 10941d05cddcSAtari911 // Three CPU/Memory bars (all update live) 10951d05cddcSAtari911 $html .= '<div class="eventlist-stats-container">'; 10961d05cddcSAtari911 10971d05cddcSAtari911 // 5-minute load average (green, updates every 2 seconds) 10981d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $calId . '(\'green\')" onmouseout="hideTooltip_' . $calId . '(\'green\')">'; 10991d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>'; 11001d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>'; 11011d05cddcSAtari911 $html .= '</div>'; 11021d05cddcSAtari911 11031d05cddcSAtari911 // Real-time CPU (purple, updates with 5-sec average) 11041d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $calId . '(\'purple\')" onmouseout="hideTooltip_' . $calId . '(\'purple\')">'; 11051d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>'; 11061d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>'; 11071d05cddcSAtari911 $html .= '</div>'; 11081d05cddcSAtari911 11091d05cddcSAtari911 // Real-time Memory (orange, updates) 11101d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $calId . '(\'orange\')" onmouseout="hideTooltip_' . $calId . '(\'orange\')">'; 11111d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>'; 11121d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>'; 11131d05cddcSAtari911 $html .= '</div>'; 11141d05cddcSAtari911 11151d05cddcSAtari911 $html .= '</div>'; 11161d05cddcSAtari911 $html .= '</div>'; 11171d05cddcSAtari911 11181d05cddcSAtari911 // Add JavaScript to update clock and weather 11191d05cddcSAtari911 $html .= '<script> 11201d05cddcSAtari911(function() { 11211d05cddcSAtari911 // Update clock every second 11221d05cddcSAtari911 function updateClock() { 11231d05cddcSAtari911 const now = new Date(); 11241d05cddcSAtari911 let hours = now.getHours(); 11251d05cddcSAtari911 const minutes = String(now.getMinutes()).padStart(2, "0"); 11261d05cddcSAtari911 const seconds = String(now.getSeconds()).padStart(2, "0"); 11271d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 11281d05cddcSAtari911 hours = hours % 12 || 12; 11291d05cddcSAtari911 const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm; 11301d05cddcSAtari911 const clockEl = document.getElementById("clock-' . $calId . '"); 11311d05cddcSAtari911 if (clockEl) clockEl.textContent = timeStr; 11321d05cddcSAtari911 } 11331d05cddcSAtari911 setInterval(updateClock, 1000); 11341d05cddcSAtari911 11351d05cddcSAtari911 // Fetch weather (geolocation-based) 11361d05cddcSAtari911 function updateWeather() { 11371d05cddcSAtari911 if ("geolocation" in navigator) { 11381d05cddcSAtari911 navigator.geolocation.getCurrentPosition(function(position) { 11391d05cddcSAtari911 const lat = position.coords.latitude; 11401d05cddcSAtari911 const lon = position.coords.longitude; 11411d05cddcSAtari911 11421d05cddcSAtari911 // Use Open-Meteo API (free, no key required) 11431d05cddcSAtari911 fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true&temperature_unit=fahrenheit`) 11441d05cddcSAtari911 .then(response => response.json()) 11451d05cddcSAtari911 .then(data => { 11461d05cddcSAtari911 if (data.current_weather) { 11471d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 11481d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 11491d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 11501d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 11511d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 11521d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 11531d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 11541d05cddcSAtari911 } 11551d05cddcSAtari911 }) 11561d05cddcSAtari911 .catch(error => { 11571d05cddcSAtari911 console.log("Weather fetch error:", error); 11581d05cddcSAtari911 }); 11591d05cddcSAtari911 }, function(error) { 11601d05cddcSAtari911 // If geolocation fails, use Sacramento as default 11611d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944¤t_weather=true&temperature_unit=fahrenheit") 11621d05cddcSAtari911 .then(response => response.json()) 11631d05cddcSAtari911 .then(data => { 11641d05cddcSAtari911 if (data.current_weather) { 11651d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 11661d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 11671d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 11681d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 11691d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 11701d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 11711d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 11721d05cddcSAtari911 } 11731d05cddcSAtari911 }) 11741d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 11751d05cddcSAtari911 }); 11761d05cddcSAtari911 } else { 11771d05cddcSAtari911 // No geolocation, use Sacramento 11781d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944¤t_weather=true&temperature_unit=fahrenheit") 11791d05cddcSAtari911 .then(response => response.json()) 11801d05cddcSAtari911 .then(data => { 11811d05cddcSAtari911 if (data.current_weather) { 11821d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 11831d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 11841d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 11851d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 11861d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 11871d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 11881d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 11891d05cddcSAtari911 } 11901d05cddcSAtari911 }) 11911d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 11921d05cddcSAtari911 } 11931d05cddcSAtari911 } 11941d05cddcSAtari911 11951d05cddcSAtari911 // WMO Weather interpretation codes 11961d05cddcSAtari911 function getWeatherIcon(code) { 11971d05cddcSAtari911 const icons = { 11981d05cddcSAtari911 0: "☀️", // Clear sky 11991d05cddcSAtari911 1: "️", // Mainly clear 12001d05cddcSAtari911 2: "⛅", // Partly cloudy 12011d05cddcSAtari911 3: "☁️", // Overcast 12021d05cddcSAtari911 45: "️", // Fog 12031d05cddcSAtari911 48: "️", // Depositing rime fog 12041d05cddcSAtari911 51: "️", // Light drizzle 12051d05cddcSAtari911 53: "️", // Moderate drizzle 12061d05cddcSAtari911 55: "️", // Dense drizzle 12071d05cddcSAtari911 61: "️", // Slight rain 12081d05cddcSAtari911 63: "️", // Moderate rain 12091d05cddcSAtari911 65: "⛈️", // Heavy rain 12101d05cddcSAtari911 71: "️", // Slight snow 12111d05cddcSAtari911 73: "️", // Moderate snow 12121d05cddcSAtari911 75: "❄️", // Heavy snow 12131d05cddcSAtari911 77: "️", // Snow grains 12141d05cddcSAtari911 80: "️", // Slight rain showers 12151d05cddcSAtari911 81: "️", // Moderate rain showers 12161d05cddcSAtari911 82: "⛈️", // Violent rain showers 12171d05cddcSAtari911 85: "️", // Slight snow showers 12181d05cddcSAtari911 86: "❄️", // Heavy snow showers 12191d05cddcSAtari911 95: "⛈️", // Thunderstorm 12201d05cddcSAtari911 96: "⛈️", // Thunderstorm with slight hail 12211d05cddcSAtari911 99: "⛈️" // Thunderstorm with heavy hail 12221d05cddcSAtari911 }; 12231d05cddcSAtari911 return icons[code] || "️"; 12241d05cddcSAtari911 } 12251d05cddcSAtari911 12261d05cddcSAtari911 // Update weather immediately and every 10 minutes 12271d05cddcSAtari911 updateWeather(); 12281d05cddcSAtari911 setInterval(updateWeather, 600000); 12291d05cddcSAtari911 12301d05cddcSAtari911 // CPU load history for 4-second rolling average 12311d05cddcSAtari911 const cpuHistory = []; 12321d05cddcSAtari911 const CPU_HISTORY_SIZE = 2; // 2 samples × 2 seconds = 4 seconds 12331d05cddcSAtari911 12341d05cddcSAtari911 // Store latest system stats for tooltips 12351d05cddcSAtari911 let latestStats = { 12361d05cddcSAtari911 load: {"1min": 0, "5min": 0, "15min": 0}, 12371d05cddcSAtari911 uptime: "", 12381d05cddcSAtari911 memory_details: {}, 12391d05cddcSAtari911 top_processes: [] 12401d05cddcSAtari911 }; 12411d05cddcSAtari911 12421d05cddcSAtari911 // Tooltip functions 12431d05cddcSAtari911 window["showTooltip_' . $calId . '"] = function(color) { 12441d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 12451d05cddcSAtari911 if (!tooltip) { 12461d05cddcSAtari911 console.log("Tooltip element not found for color:", color); 12471d05cddcSAtari911 return; 12481d05cddcSAtari911 } 12491d05cddcSAtari911 12501d05cddcSAtari911 console.log("Showing tooltip for:", color, "latestStats:", latestStats); 12511d05cddcSAtari911 12521d05cddcSAtari911 let content = ""; 12531d05cddcSAtari911 12541d05cddcSAtari911 if (color === "green") { 12551d05cddcSAtari911 // Green bar: Load averages and uptime 12561d05cddcSAtari911 content = "<div class=\"tooltip-title\">CPU Load Average</div>"; 12571d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 12581d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 12591d05cddcSAtari911 content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>"; 12601d05cddcSAtari911 if (latestStats.uptime) { 12611d05cddcSAtari911 content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\">Uptime: " + latestStats.uptime + "</div>"; 12621d05cddcSAtari911 } 12631d05cddcSAtari911 tooltip.style.borderColor = "#00cc07"; 12641d05cddcSAtari911 tooltip.style.color = "#00cc07"; 12651d05cddcSAtari911 } else if (color === "purple") { 12661d05cddcSAtari911 // Purple bar: Load averages (short-term) and top processes 12671d05cddcSAtari911 content = "<div class=\"tooltip-title\">CPU Load (Short-term)</div>"; 12681d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 12691d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 12701d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 12711d05cddcSAtari911 content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(155,89,182,0.3);\" class=\"tooltip-title\">Top Processes</div>"; 12721d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 12731d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 12741d05cddcSAtari911 }); 12751d05cddcSAtari911 } 12761d05cddcSAtari911 tooltip.style.borderColor = "#9b59b6"; 12771d05cddcSAtari911 tooltip.style.color = "#9b59b6"; 12781d05cddcSAtari911 } else if (color === "orange") { 12791d05cddcSAtari911 // Orange bar: Memory details and top processes 12801d05cddcSAtari911 content = "<div class=\"tooltip-title\">Memory Usage</div>"; 12811d05cddcSAtari911 if (latestStats.memory_details && latestStats.memory_details.total) { 12821d05cddcSAtari911 content += "<div>Total: " + latestStats.memory_details.total + "</div>"; 12831d05cddcSAtari911 content += "<div>Used: " + latestStats.memory_details.used + "</div>"; 12841d05cddcSAtari911 content += "<div>Available: " + latestStats.memory_details.available + "</div>"; 12851d05cddcSAtari911 if (latestStats.memory_details.cached) { 12861d05cddcSAtari911 content += "<div>Cached: " + latestStats.memory_details.cached + "</div>"; 12871d05cddcSAtari911 } 12881d05cddcSAtari911 } else { 12891d05cddcSAtari911 content += "<div>Loading...</div>"; 12901d05cddcSAtari911 } 12911d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 12921d05cddcSAtari911 content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(255,140,0,0.3);\" class=\"tooltip-title\">Top Processes</div>"; 12931d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 12941d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 12951d05cddcSAtari911 }); 12961d05cddcSAtari911 } 12971d05cddcSAtari911 tooltip.style.borderColor = "#ff9800"; 12981d05cddcSAtari911 tooltip.style.color = "#ff9800"; 12991d05cddcSAtari911 } 13001d05cddcSAtari911 13011d05cddcSAtari911 console.log("Tooltip content:", content); 13021d05cddcSAtari911 tooltip.innerHTML = content; 13031d05cddcSAtari911 tooltip.style.display = "block"; 13041d05cddcSAtari911 13051d05cddcSAtari911 // Position tooltip using fixed positioning above the bar 13061d05cddcSAtari911 const bar = tooltip.parentElement; 13071d05cddcSAtari911 const barRect = bar.getBoundingClientRect(); 13081d05cddcSAtari911 const tooltipRect = tooltip.getBoundingClientRect(); 13091d05cddcSAtari911 13101d05cddcSAtari911 // Center horizontally on the bar 13111d05cddcSAtari911 const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2); 13121d05cddcSAtari911 // Position above the bar with 8px gap 13131d05cddcSAtari911 const top = barRect.top - tooltipRect.height - 8; 13141d05cddcSAtari911 13151d05cddcSAtari911 tooltip.style.left = left + "px"; 13161d05cddcSAtari911 tooltip.style.top = top + "px"; 13171d05cddcSAtari911 }; 13181d05cddcSAtari911 13191d05cddcSAtari911 window["hideTooltip_' . $calId . '"] = function(color) { 13201d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 13211d05cddcSAtari911 if (tooltip) { 13221d05cddcSAtari911 tooltip.style.display = "none"; 13231d05cddcSAtari911 } 13241d05cddcSAtari911 }; 13251d05cddcSAtari911 13261d05cddcSAtari911 // Update CPU and memory bars every 2 seconds 13271d05cddcSAtari911 function updateSystemStats() { 13281d05cddcSAtari911 // Fetch real system stats from server 13291d05cddcSAtari911 fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php") 13301d05cddcSAtari911 .then(response => response.json()) 13311d05cddcSAtari911 .then(data => { 13321d05cddcSAtari911 console.log("System stats received:", data); 13331d05cddcSAtari911 13341d05cddcSAtari911 // Store data for tooltips 13351d05cddcSAtari911 latestStats = { 13361d05cddcSAtari911 load: data.load || {"1min": 0, "5min": 0, "15min": 0}, 13371d05cddcSAtari911 uptime: data.uptime || "", 13381d05cddcSAtari911 memory_details: data.memory_details || {}, 13391d05cddcSAtari911 top_processes: data.top_processes || [] 13401d05cddcSAtari911 }; 13411d05cddcSAtari911 13421d05cddcSAtari911 console.log("latestStats updated to:", latestStats); 13431d05cddcSAtari911 13441d05cddcSAtari911 // Update green bar (5-minute average) - updates live now! 13451d05cddcSAtari911 const greenBar = document.getElementById("cpu-5min-' . $calId . '"); 13461d05cddcSAtari911 if (greenBar) { 13471d05cddcSAtari911 greenBar.style.width = Math.min(100, data.cpu_5min) + "%"; 13481d05cddcSAtari911 } 13491d05cddcSAtari911 13501d05cddcSAtari911 // Add current CPU to history for purple bar 13511d05cddcSAtari911 cpuHistory.push(data.cpu); 13521d05cddcSAtari911 if (cpuHistory.length > CPU_HISTORY_SIZE) { 13531d05cddcSAtari911 cpuHistory.shift(); // Remove oldest 13541d05cddcSAtari911 } 13551d05cddcSAtari911 13561d05cddcSAtari911 // Calculate 5-second average for CPU 13571d05cddcSAtari911 const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length; 13581d05cddcSAtari911 13591d05cddcSAtari911 // Update CPU bar (purple) with 5-second average 13601d05cddcSAtari911 const cpuBar = document.getElementById("cpu-realtime-' . $calId . '"); 13611d05cddcSAtari911 if (cpuBar) { 13621d05cddcSAtari911 cpuBar.style.width = Math.min(100, cpuAverage) + "%"; 13631d05cddcSAtari911 } 13641d05cddcSAtari911 13651d05cddcSAtari911 // Update memory bar (orange) with real data 13661d05cddcSAtari911 const memBar = document.getElementById("mem-realtime-' . $calId . '"); 13671d05cddcSAtari911 if (memBar) { 13681d05cddcSAtari911 memBar.style.width = Math.min(100, data.memory) + "%"; 13691d05cddcSAtari911 } 13701d05cddcSAtari911 }) 13711d05cddcSAtari911 .catch(error => { 13721d05cddcSAtari911 console.log("System stats error:", error); 13731d05cddcSAtari911 // Fallback to client-side estimates on error 13741d05cddcSAtari911 const cpuFallback = Math.random() * 100; 13751d05cddcSAtari911 cpuHistory.push(cpuFallback); 13761d05cddcSAtari911 if (cpuHistory.length > CPU_HISTORY_SIZE) { 13771d05cddcSAtari911 cpuHistory.shift(); 13781d05cddcSAtari911 } 13791d05cddcSAtari911 const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length; 13801d05cddcSAtari911 13811d05cddcSAtari911 const greenBar = document.getElementById("cpu-5min-' . $calId . '"); 13821d05cddcSAtari911 if (greenBar) greenBar.style.width = Math.min(100, cpuFallback) + "%"; 13831d05cddcSAtari911 13841d05cddcSAtari911 const cpuBar = document.getElementById("cpu-realtime-' . $calId . '"); 13851d05cddcSAtari911 if (cpuBar) cpuBar.style.width = Math.min(100, cpuAverage) + "%"; 13861d05cddcSAtari911 13871d05cddcSAtari911 let memoryUsage = 0; 13881d05cddcSAtari911 if (performance.memory) { 13891d05cddcSAtari911 memoryUsage = (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100; 13901d05cddcSAtari911 } else { 13911d05cddcSAtari911 memoryUsage = Math.random() * 100; 13921d05cddcSAtari911 } 13931d05cddcSAtari911 const memBar = document.getElementById("mem-realtime-' . $calId . '"); 13941d05cddcSAtari911 if (memBar) memBar.style.width = Math.min(100, memoryUsage) + "%"; 13951d05cddcSAtari911 }); 13961d05cddcSAtari911 } 13971d05cddcSAtari911 13981d05cddcSAtari911 // Update immediately and then every 2 seconds 13991d05cddcSAtari911 updateSystemStats(); 14001d05cddcSAtari911 setInterval(updateSystemStats, 2000); 14011d05cddcSAtari911})(); 14021d05cddcSAtari911</script>'; 14031d05cddcSAtari911 } 140419378907SAtari911 140519378907SAtari911 if (empty($allEvents)) { 1406e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-empty">'; 1407e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText); 1408e3a9f44cSAtari911 if ($namespace) { 1409e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>'; 141087ac9bf3SAtari911 } 1411e3a9f44cSAtari911 $html .= '</div>'; 1412e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-body">No events</div>'; 1413e3a9f44cSAtari911 $html .= '</div>'; 1414e3a9f44cSAtari911 } else { 1415e3a9f44cSAtari911 // Calculate today and tomorrow's dates for highlighting 14161d05cddcSAtari911 $todayStr = date('Y-m-d'); 1417e3a9f44cSAtari911 $tomorrow = date('Y-m-d', strtotime('+1 day')); 1418e3a9f44cSAtari911 1419e3a9f44cSAtari911 foreach ($allEvents as $dateKey => $dayEvents) { 1420e3a9f44cSAtari911 $dateObj = new DateTime($dateKey); 1421e3a9f44cSAtari911 $displayDate = $dateObj->format('D, M j'); 1422e3a9f44cSAtari911 14231d05cddcSAtari911 // Check if this date is today or tomorrow or past 1424e3a9f44cSAtari911 // Enable highlighting for sidebar mode AND range modes (day, week, month) 1425e3a9f44cSAtari911 $enableHighlighting = $sidebar || !empty($range); 14261d05cddcSAtari911 $isToday = $enableHighlighting && ($dateKey === $todayStr); 1427e3a9f44cSAtari911 $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow); 14281d05cddcSAtari911 $isPast = $dateKey < $todayStr; 142919378907SAtari911 143019378907SAtari911 foreach ($dayEvents as $event) { 14311d05cddcSAtari911 // Check if this is a task and if it's completed 14321d05cddcSAtari911 $isTask = !empty($event['isTask']); 14331d05cddcSAtari911 $completed = !empty($event['completed']); 14341d05cddcSAtari911 14351d05cddcSAtari911 // ALWAYS skip completed tasks UNLESS showchecked is explicitly set 14361d05cddcSAtari911 if (!$showchecked && $isTask && $completed) { 1437e3a9f44cSAtari911 continue; 1438e3a9f44cSAtari911 } 143919378907SAtari911 14401d05cddcSAtari911 // Skip past events that are NOT tasks (only show past due tasks from the past) 14411d05cddcSAtari911 if ($isPast && !$isTask) { 14421d05cddcSAtari911 continue; 14431d05cddcSAtari911 } 14441d05cddcSAtari911 14451d05cddcSAtari911 // Determine if task is past due (past date, is task, not completed) 14461d05cddcSAtari911 $isPastDue = $isPast && $isTask && !$completed; 14471d05cddcSAtari911 1448e3a9f44cSAtari911 // Line 1: Header (Title, Time, Date, Namespace) 1449e3a9f44cSAtari911 $todayClass = $isToday ? ' eventlist-simple-today' : ''; 1450e3a9f44cSAtari911 $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : ''; 14511d05cddcSAtari911 $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : ''; 14521d05cddcSAtari911 $html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . $pastDueClass . '">'; 1453e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-header">'; 1454e3a9f44cSAtari911 1455e3a9f44cSAtari911 // Title 1456e3a9f44cSAtari911 $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>'; 1457e3a9f44cSAtari911 1458e3a9f44cSAtari911 // Time (12-hour format) 1459e3a9f44cSAtari911 if (!empty($event['time'])) { 1460e3a9f44cSAtari911 $timeParts = explode(':', $event['time']); 146187ac9bf3SAtari911 if (count($timeParts) === 2) { 146287ac9bf3SAtari911 $hour = (int)$timeParts[0]; 146387ac9bf3SAtari911 $minute = $timeParts[1]; 146487ac9bf3SAtari911 $ampm = $hour >= 12 ? 'PM' : 'AM'; 1465e3a9f44cSAtari911 $hour = $hour % 12 ?: 12; 146687ac9bf3SAtari911 $displayTime = $hour . ':' . $minute . ' ' . $ampm; 1467e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>'; 146819378907SAtari911 } 146987ac9bf3SAtari911 } 147087ac9bf3SAtari911 1471e3a9f44cSAtari911 // Date 1472e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>'; 1473e3a9f44cSAtari911 14741d05cddcSAtari911 // Badge: PAST DUE, TODAY, or nothing 14751d05cddcSAtari911 if ($isPastDue) { 14761d05cddcSAtari911 $html .= ' <span class="eventlist-simple-pastdue-badge">PAST DUE</span>'; 14771d05cddcSAtari911 } elseif ($isToday) { 1478e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-today-badge">TODAY</span>'; 147987ac9bf3SAtari911 } 1480e3a9f44cSAtari911 1481e3a9f44cSAtari911 // Namespace badge (show individual event's namespace) 1482e3a9f44cSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 1483e3a9f44cSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 1484e3a9f44cSAtari911 $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading 148519378907SAtari911 } 1486e3a9f44cSAtari911 if ($eventNamespace) { 1487e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</span>'; 1488e3a9f44cSAtari911 } 1489e3a9f44cSAtari911 1490e3a9f44cSAtari911 $html .= '</div>'; // header 1491e3a9f44cSAtari911 1492e3a9f44cSAtari911 // Line 2: Body (Description only) - only show if description exists 1493e3a9f44cSAtari911 if (!empty($event['description'])) { 1494e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) . '</div>'; 1495e3a9f44cSAtari911 } 1496e3a9f44cSAtari911 1497e3a9f44cSAtari911 $html .= '</div>'; // item 149819378907SAtari911 } 149919378907SAtari911 } 150087ac9bf3SAtari911 } 150119378907SAtari911 1502e3a9f44cSAtari911 $html .= '</div>'; // eventlist-simple 150319378907SAtari911 150419378907SAtari911 return $html; 150519378907SAtari911 } 150619378907SAtari911 1507*0c3b6e81SAtari911 private function renderEventDialog($calId, $namespace, $theme = null) { 15089ccd446eSAtari911 // Get theme for dialog 1509*0c3b6e81SAtari911 if ($theme === null) { 15109ccd446eSAtari911 $theme = $this->getSidebarTheme(); 1511*0c3b6e81SAtari911 } 15129ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 15139ccd446eSAtari911 151419378907SAtari911 $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">'; 151519378907SAtari911 $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>'; 151619378907SAtari911 15179ccd446eSAtari911 // Draggable dialog with theme 151819378907SAtari911 $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">'; 151919378907SAtari911 152019378907SAtari911 // Header with drag handle and close button 152119378907SAtari911 $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">'; 152219378907SAtari911 $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>'; 152319378907SAtari911 $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>'; 152419378907SAtari911 $html .= '</div>'; 152519378907SAtari911 152619378907SAtari911 // Form content 152719378907SAtari911 $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">'; 152819378907SAtari911 152919378907SAtari911 // Hidden ID field 153019378907SAtari911 $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">'; 153119378907SAtari911 15321d05cddcSAtari911 // 1. TITLE 15331d05cddcSAtari911 $html .= '<div class="form-field">'; 15341d05cddcSAtari911 $html .= '<label class="field-label"> Title</label>'; 15351d05cddcSAtari911 $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek input-compact" placeholder="Event or task title...">'; 153619378907SAtari911 $html .= '</div>'; 153719378907SAtari911 15381d05cddcSAtari911 // 1.5 NAMESPACE SELECTOR (Searchable with fuzzy matching) 15391d05cddcSAtari911 $html .= '<div class="form-field">'; 15401d05cddcSAtari911 $html .= '<label class="field-label"> Namespace</label>'; 15411d05cddcSAtari911 15421d05cddcSAtari911 // Hidden field to store actual selected namespace 15431d05cddcSAtari911 $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">'; 15441d05cddcSAtari911 15451d05cddcSAtari911 // Searchable input 15461d05cddcSAtari911 $html .= '<div class="namespace-search-wrapper">'; 15471d05cddcSAtari911 $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">'; 15481d05cddcSAtari911 $html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="display:none;"></div>'; 15491d05cddcSAtari911 $html .= '</div>'; 15501d05cddcSAtari911 15511d05cddcSAtari911 // Store namespaces as JSON for JavaScript 15521d05cddcSAtari911 $allNamespaces = $this->getAllNamespaces(); 15531d05cddcSAtari911 $html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($allNamespaces) . '</script>'; 15541d05cddcSAtari911 15551d05cddcSAtari911 $html .= '</div>'; 15561d05cddcSAtari911 15571d05cddcSAtari911 // 2. DESCRIPTION 15581d05cddcSAtari911 $html .= '<div class="form-field">'; 15591d05cddcSAtari911 $html .= '<label class="field-label"> Description</label>'; 15609ccd446eSAtari911 $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="2" class="input-sleek textarea-sleek textarea-compact" placeholder="Optional details..."></textarea>'; 15611d05cddcSAtari911 $html .= '</div>'; 15621d05cddcSAtari911 15631d05cddcSAtari911 // 3. START DATE - END DATE (inline) 156419378907SAtari911 $html .= '<div class="form-row-group">'; 156519378907SAtari911 15661d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 15671d05cddcSAtari911 $html .= '<label class="field-label-compact"> Start Date</label>'; 15681d05cddcSAtari911 $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date input-compact">'; 156919378907SAtari911 $html .= '</div>'; 157019378907SAtari911 15711d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 15721d05cddcSAtari911 $html .= '<label class="field-label-compact"> End Date</label>'; 15731d05cddcSAtari911 $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date input-compact" placeholder="Optional">'; 157419378907SAtari911 $html .= '</div>'; 157519378907SAtari911 15761d05cddcSAtari911 $html .= '</div>'; // End row 157719378907SAtari911 15781d05cddcSAtari911 // 4. IS REPEATING CHECKBOX 15791d05cddcSAtari911 $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">'; 15801d05cddcSAtari911 $html .= '<label class="checkbox-label checkbox-label-compact">'; 158187ac9bf3SAtari911 $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">'; 158287ac9bf3SAtari911 $html .= '<span> Repeating Event</span>'; 158387ac9bf3SAtari911 $html .= '</label>'; 158487ac9bf3SAtari911 $html .= '</div>'; 158587ac9bf3SAtari911 15861d05cddcSAtari911 // Recurring options (shown when checkbox is checked) 158787ac9bf3SAtari911 $html .= '<div id="recurring-options-' . $calId . '" class="recurring-options" style="display:none;">'; 158887ac9bf3SAtari911 15891d05cddcSAtari911 $html .= '<div class="form-row-group">'; 15901d05cddcSAtari911 15911d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 15921d05cddcSAtari911 $html .= '<label class="field-label-compact">Repeat Every</label>'; 15931d05cddcSAtari911 $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek input-compact">'; 159487ac9bf3SAtari911 $html .= '<option value="daily">Daily</option>'; 159587ac9bf3SAtari911 $html .= '<option value="weekly">Weekly</option>'; 159687ac9bf3SAtari911 $html .= '<option value="monthly">Monthly</option>'; 159787ac9bf3SAtari911 $html .= '<option value="yearly">Yearly</option>'; 159887ac9bf3SAtari911 $html .= '</select>'; 159987ac9bf3SAtari911 $html .= '</div>'; 160087ac9bf3SAtari911 16011d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 16021d05cddcSAtari911 $html .= '<label class="field-label-compact">Repeat Until</label>'; 16031d05cddcSAtari911 $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date input-compact" placeholder="Optional">'; 160487ac9bf3SAtari911 $html .= '</div>'; 160587ac9bf3SAtari911 16061d05cddcSAtari911 $html .= '</div>'; // End row 16071d05cddcSAtari911 $html .= '</div>'; // End recurring options 160887ac9bf3SAtari911 16091d05cddcSAtari911 // 5. TIME (Start & End) - COLOR (inline) 16101d05cddcSAtari911 $html .= '<div class="form-row-group">'; 16111d05cddcSAtari911 16121d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 16131d05cddcSAtari911 $html .= '<label class="field-label-compact"> Start Time</label>'; 16141d05cddcSAtari911 $html .= '<select id="event-time-' . $calId . '" name="time" class="input-sleek input-compact" onchange="updateEndTimeOptions(\'' . $calId . '\')">'; 16151d05cddcSAtari911 $html .= '<option value="">All day</option>'; 1616e3a9f44cSAtari911 1617e3a9f44cSAtari911 // Generate time options in 15-minute intervals 1618e3a9f44cSAtari911 for ($hour = 0; $hour < 24; $hour++) { 1619e3a9f44cSAtari911 for ($minute = 0; $minute < 60; $minute += 15) { 1620e3a9f44cSAtari911 $timeValue = sprintf('%02d:%02d', $hour, $minute); 1621e3a9f44cSAtari911 $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour); 1622e3a9f44cSAtari911 $ampm = $hour < 12 ? 'AM' : 'PM'; 1623e3a9f44cSAtari911 $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm); 1624e3a9f44cSAtari911 $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>'; 1625e3a9f44cSAtari911 } 1626e3a9f44cSAtari911 } 1627e3a9f44cSAtari911 1628e3a9f44cSAtari911 $html .= '</select>'; 162919378907SAtari911 $html .= '</div>'; 163019378907SAtari911 16311d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 16321d05cddcSAtari911 $html .= '<label class="field-label-compact"> End Time</label>'; 16331d05cddcSAtari911 $html .= '<select id="event-end-time-' . $calId . '" name="endTime" class="input-sleek input-compact">'; 16341d05cddcSAtari911 $html .= '<option value="">Same as start</option>'; 16351d05cddcSAtari911 16361d05cddcSAtari911 // Generate time options in 15-minute intervals 16371d05cddcSAtari911 for ($hour = 0; $hour < 24; $hour++) { 16381d05cddcSAtari911 for ($minute = 0; $minute < 60; $minute += 15) { 16391d05cddcSAtari911 $timeValue = sprintf('%02d:%02d', $hour, $minute); 16401d05cddcSAtari911 $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour); 16411d05cddcSAtari911 $ampm = $hour < 12 ? 'AM' : 'PM'; 16421d05cddcSAtari911 $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm); 16431d05cddcSAtari911 $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>'; 16441d05cddcSAtari911 } 16451d05cddcSAtari911 } 16461d05cddcSAtari911 16471d05cddcSAtari911 $html .= '</select>'; 164819378907SAtari911 $html .= '</div>'; 164919378907SAtari911 16501d05cddcSAtari911 $html .= '</div>'; // End row 16511d05cddcSAtari911 16521d05cddcSAtari911 // Color field (new row) 16531d05cddcSAtari911 $html .= '<div class="form-row-group">'; 16541d05cddcSAtari911 16551d05cddcSAtari911 $html .= '<div class="form-field form-field-full">'; 16561d05cddcSAtari911 $html .= '<label class="field-label-compact"> Color</label>'; 16571d05cddcSAtari911 $html .= '<div class="color-picker-wrapper">'; 16581d05cddcSAtari911 $html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact color-select" onchange="updateCustomColorPicker(\'' . $calId . '\')">'; 16591d05cddcSAtari911 $html .= '<option value="#3498db" style="background:#3498db;color:white"> Blue</option>'; 16601d05cddcSAtari911 $html .= '<option value="#2ecc71" style="background:#2ecc71;color:white"> Green</option>'; 16611d05cddcSAtari911 $html .= '<option value="#e74c3c" style="background:#e74c3c;color:white"> Red</option>'; 16621d05cddcSAtari911 $html .= '<option value="#f39c12" style="background:#f39c12;color:white"> Orange</option>'; 16631d05cddcSAtari911 $html .= '<option value="#9b59b6" style="background:#9b59b6;color:white"> Purple</option>'; 16641d05cddcSAtari911 $html .= '<option value="#e91e63" style="background:#e91e63;color:white"> Pink</option>'; 16651d05cddcSAtari911 $html .= '<option value="#1abc9c" style="background:#1abc9c;color:white"> Teal</option>'; 16661d05cddcSAtari911 $html .= '<option value="custom"> Custom...</option>'; 16671d05cddcSAtari911 $html .= '</select>'; 16681d05cddcSAtari911 $html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input color-picker-compact" value="#3498db" onchange="updateColorFromPicker(\'' . $calId . '\')">'; 16691d05cddcSAtari911 $html .= '</div>'; 167019378907SAtari911 $html .= '</div>'; 167119378907SAtari911 16721d05cddcSAtari911 $html .= '</div>'; // End row 16731d05cddcSAtari911 16741d05cddcSAtari911 // Task checkbox 16751d05cddcSAtari911 $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">'; 16761d05cddcSAtari911 $html .= '<label class="checkbox-label checkbox-label-compact">'; 16771d05cddcSAtari911 $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">'; 16781d05cddcSAtari911 $html .= '<span> This is a task (can be checked off)</span>'; 16791d05cddcSAtari911 $html .= '</label>'; 168019378907SAtari911 $html .= '</div>'; 168119378907SAtari911 168219378907SAtari911 // Action buttons 168319378907SAtari911 $html .= '<div class="dialog-actions-sleek">'; 168419378907SAtari911 $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>'; 168519378907SAtari911 $html .= '<button type="submit" class="btn-sleek btn-save-sleek"> Save</button>'; 168619378907SAtari911 $html .= '</div>'; 168719378907SAtari911 168819378907SAtari911 $html .= '</form>'; 168919378907SAtari911 $html .= '</div>'; 169019378907SAtari911 $html .= '</div>'; 169119378907SAtari911 169219378907SAtari911 return $html; 169319378907SAtari911 } 169419378907SAtari911 16959ccd446eSAtari911 private function renderMonthPicker($calId, $year, $month, $namespace, $theme = 'matrix', $themeStyles = null) { 16969ccd446eSAtari911 // Fallback to default theme if not provided 16979ccd446eSAtari911 if ($themeStyles === null) { 16989ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 16999ccd446eSAtari911 } 17009ccd446eSAtari911 17019ccd446eSAtari911 $themeClass = 'calendar-theme-' . $theme; 17029ccd446eSAtari911 17039ccd446eSAtari911 $html = '<div class="month-picker-overlay ' . $themeClass . '" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">'; 170487ac9bf3SAtari911 $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">'; 170587ac9bf3SAtari911 $html .= '<h4>Jump to Month</h4>'; 170687ac9bf3SAtari911 170787ac9bf3SAtari911 $html .= '<div class="month-picker-selects">'; 170887ac9bf3SAtari911 $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">'; 170987ac9bf3SAtari911 $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 171087ac9bf3SAtari911 for ($m = 1; $m <= 12; $m++) { 171187ac9bf3SAtari911 $selected = ($m == $month) ? ' selected' : ''; 171287ac9bf3SAtari911 $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>'; 171387ac9bf3SAtari911 } 171487ac9bf3SAtari911 $html .= '</select>'; 171587ac9bf3SAtari911 171687ac9bf3SAtari911 $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">'; 171787ac9bf3SAtari911 $currentYear = (int)date('Y'); 171887ac9bf3SAtari911 for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) { 171987ac9bf3SAtari911 $selected = ($y == $year) ? ' selected' : ''; 172087ac9bf3SAtari911 $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>'; 172187ac9bf3SAtari911 } 172287ac9bf3SAtari911 $html .= '</select>'; 172387ac9bf3SAtari911 $html .= '</div>'; 172487ac9bf3SAtari911 172587ac9bf3SAtari911 $html .= '<div class="month-picker-actions">'; 172687ac9bf3SAtari911 $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>'; 172787ac9bf3SAtari911 $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>'; 172887ac9bf3SAtari911 $html .= '</div>'; 172987ac9bf3SAtari911 173087ac9bf3SAtari911 $html .= '</div>'; 173187ac9bf3SAtari911 $html .= '</div>'; 173287ac9bf3SAtari911 173387ac9bf3SAtari911 return $html; 173487ac9bf3SAtari911 } 173587ac9bf3SAtari911 17369ccd446eSAtari911 private function renderDescription($description, $themeStyles = null) { 173719378907SAtari911 if (empty($description)) { 173819378907SAtari911 return ''; 173919378907SAtari911 } 174019378907SAtari911 17419ccd446eSAtari911 // Get theme for link colors if not provided 17429ccd446eSAtari911 if ($themeStyles === null) { 17439ccd446eSAtari911 $theme = $this->getSidebarTheme(); 17449ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 17459ccd446eSAtari911 } 17469ccd446eSAtari911 17479ccd446eSAtari911 $linkColor = ''; 17489ccd446eSAtari911 $linkStyle = ' class="cal-link"'; 17499ccd446eSAtari911 1750e3a9f44cSAtari911 // Token-based parsing to avoid escaping issues 1751e3a9f44cSAtari911 $rendered = $description; 1752e3a9f44cSAtari911 $tokens = array(); 1753e3a9f44cSAtari911 $tokenIndex = 0; 175419378907SAtari911 1755e3a9f44cSAtari911 // Convert DokuWiki image syntax {{image.jpg}} to tokens 1756e3a9f44cSAtari911 $pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/'; 1757e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 1758e3a9f44cSAtari911 foreach ($matches as $match) { 1759e3a9f44cSAtari911 $imagePath = trim($match[1]); 1760e3a9f44cSAtari911 $alt = isset($match[2]) ? trim($match[2]) : ''; 176119378907SAtari911 1762e3a9f44cSAtari911 // Handle external URLs 176319378907SAtari911 if (preg_match('/^https?:\/\//', $imagePath)) { 1764e3a9f44cSAtari911 $imageHtml = '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 1765e3a9f44cSAtari911 } else { 176619378907SAtari911 // Handle internal DokuWiki images 176719378907SAtari911 $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath); 1768e3a9f44cSAtari911 $imageHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 1769e3a9f44cSAtari911 } 177019378907SAtari911 1771e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 1772e3a9f44cSAtari911 $tokens[$tokenIndex] = $imageHtml; 1773e3a9f44cSAtari911 $tokenIndex++; 1774e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 1775e3a9f44cSAtari911 } 1776e3a9f44cSAtari911 1777e3a9f44cSAtari911 // Convert DokuWiki link syntax [[link|text]] to tokens 1778e3a9f44cSAtari911 $pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/'; 1779e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 1780e3a9f44cSAtari911 foreach ($matches as $match) { 1781e3a9f44cSAtari911 $link = trim($match[1]); 1782e3a9f44cSAtari911 $text = isset($match[2]) ? trim($match[2]) : $link; 178319378907SAtari911 178419378907SAtari911 // Handle external URLs 178519378907SAtari911 if (preg_match('/^https?:\/\//', $link)) { 17869ccd446eSAtari911 $linkHtml = '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>'; 1787e3a9f44cSAtari911 } else { 178887ac9bf3SAtari911 // Handle internal DokuWiki links with section anchors 178987ac9bf3SAtari911 $parts = explode('#', $link, 2); 179087ac9bf3SAtari911 $pagePart = $parts[0]; 179187ac9bf3SAtari911 $sectionPart = isset($parts[1]) ? '#' . $parts[1] : ''; 179287ac9bf3SAtari911 179387ac9bf3SAtari911 $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart; 17949ccd446eSAtari911 $linkHtml = '<a href="' . $wikiUrl . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>'; 179519378907SAtari911 } 179619378907SAtari911 1797e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 1798e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 1799e3a9f44cSAtari911 $tokenIndex++; 1800e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 1801e3a9f44cSAtari911 } 180219378907SAtari911 1803e3a9f44cSAtari911 // Convert markdown-style links [text](url) to tokens 1804e3a9f44cSAtari911 $pattern = '/\[([^\]]+)\]\(([^)]+)\)/'; 1805e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 1806e3a9f44cSAtari911 foreach ($matches as $match) { 1807e3a9f44cSAtari911 $text = trim($match[1]); 1808e3a9f44cSAtari911 $url = trim($match[2]); 180919378907SAtari911 1810e3a9f44cSAtari911 if (preg_match('/^https?:\/\//', $url)) { 18119ccd446eSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>'; 1812e3a9f44cSAtari911 } else { 18139ccd446eSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>'; 1814e3a9f44cSAtari911 } 1815e3a9f44cSAtari911 1816e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 1817e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 1818e3a9f44cSAtari911 $tokenIndex++; 1819e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 1820e3a9f44cSAtari911 } 1821e3a9f44cSAtari911 1822e3a9f44cSAtari911 // Convert plain URLs to tokens 1823e3a9f44cSAtari911 $pattern = '/(https?:\/\/[^\s<]+)/'; 1824e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 1825e3a9f44cSAtari911 foreach ($matches as $match) { 1826e3a9f44cSAtari911 $url = $match[1]; 18279ccd446eSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($url) . '</a>'; 1828e3a9f44cSAtari911 1829e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 1830e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 1831e3a9f44cSAtari911 $tokenIndex++; 1832e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 1833e3a9f44cSAtari911 } 1834e3a9f44cSAtari911 1835e3a9f44cSAtari911 // NOW escape HTML (tokens are protected) 1836e3a9f44cSAtari911 $rendered = htmlspecialchars($rendered); 1837e3a9f44cSAtari911 1838e3a9f44cSAtari911 // Convert newlines to <br> 1839e3a9f44cSAtari911 $rendered = nl2br($rendered); 1840e3a9f44cSAtari911 1841e3a9f44cSAtari911 // DokuWiki text formatting 1842e3a9f44cSAtari911 // Bold: **text** or __text__ 18439ccd446eSAtari911 $boldStyle = ''; 1844e3a9f44cSAtari911 $rendered = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $rendered); 1845e3a9f44cSAtari911 $rendered = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $rendered); 1846e3a9f44cSAtari911 1847e3a9f44cSAtari911 // Italic: //text// 1848e3a9f44cSAtari911 $rendered = preg_replace('/\/\/(.+?)\/\//', '<em>$1</em>', $rendered); 1849e3a9f44cSAtari911 1850e3a9f44cSAtari911 // Strikethrough: <del>text</del> 1851e3a9f44cSAtari911 $rendered = preg_replace('/<del>(.+?)<\/del>/', '<del>$1</del>', $rendered); 1852e3a9f44cSAtari911 1853e3a9f44cSAtari911 // Monospace: ''text'' 1854e3a9f44cSAtari911 $rendered = preg_replace('/''(.+?)''/', '<code>$1</code>', $rendered); 1855e3a9f44cSAtari911 1856e3a9f44cSAtari911 // Subscript: <sub>text</sub> 1857e3a9f44cSAtari911 $rendered = preg_replace('/<sub>(.+?)<\/sub>/', '<sub>$1</sub>', $rendered); 1858e3a9f44cSAtari911 1859e3a9f44cSAtari911 // Superscript: <sup>text</sup> 1860e3a9f44cSAtari911 $rendered = preg_replace('/<sup>(.+?)<\/sup>/', '<sup>$1</sup>', $rendered); 1861e3a9f44cSAtari911 1862e3a9f44cSAtari911 // Restore tokens 1863e3a9f44cSAtari911 foreach ($tokens as $i => $html) { 1864e3a9f44cSAtari911 $rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered); 1865e3a9f44cSAtari911 } 186619378907SAtari911 186719378907SAtari911 return $rendered; 186819378907SAtari911 } 186919378907SAtari911 187019378907SAtari911 private function loadEvents($namespace, $year, $month) { 187119378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 187219378907SAtari911 if ($namespace) { 187319378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 187419378907SAtari911 } 187519378907SAtari911 $dataDir .= 'calendar/'; 187619378907SAtari911 187719378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 187819378907SAtari911 187919378907SAtari911 if (file_exists($eventFile)) { 188019378907SAtari911 $json = file_get_contents($eventFile); 188119378907SAtari911 return json_decode($json, true); 188219378907SAtari911 } 188319378907SAtari911 188419378907SAtari911 return array(); 188519378907SAtari911 } 1886e3a9f44cSAtari911 1887e3a9f44cSAtari911 private function loadEventsMultiNamespace($namespaces, $year, $month) { 1888e3a9f44cSAtari911 // Check for wildcard pattern (namespace:*) 1889e3a9f44cSAtari911 if (preg_match('/^(.+):\*$/', $namespaces, $matches)) { 1890e3a9f44cSAtari911 $baseNamespace = $matches[1]; 1891e3a9f44cSAtari911 return $this->loadEventsWildcard($baseNamespace, $year, $month); 1892e3a9f44cSAtari911 } 1893e3a9f44cSAtari911 1894e3a9f44cSAtari911 // Check for root wildcard (just *) 1895e3a9f44cSAtari911 if ($namespaces === '*') { 1896e3a9f44cSAtari911 return $this->loadEventsWildcard('', $year, $month); 1897e3a9f44cSAtari911 } 1898e3a9f44cSAtari911 1899e3a9f44cSAtari911 // Parse namespace list (semicolon separated) 1900e3a9f44cSAtari911 // e.g., "team:projects;personal;work:tasks" = three namespaces 1901e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespaces)); 1902e3a9f44cSAtari911 1903e3a9f44cSAtari911 // Load events from all namespaces 1904e3a9f44cSAtari911 $allEvents = array(); 1905e3a9f44cSAtari911 foreach ($namespaceList as $ns) { 1906e3a9f44cSAtari911 $ns = trim($ns); 1907e3a9f44cSAtari911 if (empty($ns)) continue; 1908e3a9f44cSAtari911 1909e3a9f44cSAtari911 $events = $this->loadEvents($ns, $year, $month); 1910e3a9f44cSAtari911 1911e3a9f44cSAtari911 // Add namespace tag to each event 1912e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1913e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1914e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1915e3a9f44cSAtari911 } 1916e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1917e3a9f44cSAtari911 $event['_namespace'] = $ns; 1918e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1919e3a9f44cSAtari911 } 1920e3a9f44cSAtari911 } 1921e3a9f44cSAtari911 } 1922e3a9f44cSAtari911 1923e3a9f44cSAtari911 return $allEvents; 1924e3a9f44cSAtari911 } 1925e3a9f44cSAtari911 1926e3a9f44cSAtari911 private function loadEventsWildcard($baseNamespace, $year, $month) { 1927e3a9f44cSAtari911 // Find all subdirectories under the base namespace 1928e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 1929e3a9f44cSAtari911 if ($baseNamespace) { 1930e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 1931e3a9f44cSAtari911 } 1932e3a9f44cSAtari911 1933e3a9f44cSAtari911 $allEvents = array(); 1934e3a9f44cSAtari911 1935e3a9f44cSAtari911 // First, load events from the base namespace itself 1936e3a9f44cSAtari911 if (empty($baseNamespace)) { 1937e3a9f44cSAtari911 // Root wildcard - load from root calendar 1938e3a9f44cSAtari911 $events = $this->loadEvents('', $year, $month); 1939e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1940e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1941e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1942e3a9f44cSAtari911 } 1943e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1944e3a9f44cSAtari911 $event['_namespace'] = ''; 1945e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1946e3a9f44cSAtari911 } 1947e3a9f44cSAtari911 } 1948e3a9f44cSAtari911 } else { 1949e3a9f44cSAtari911 $events = $this->loadEvents($baseNamespace, $year, $month); 1950e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1951e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1952e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1953e3a9f44cSAtari911 } 1954e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1955e3a9f44cSAtari911 $event['_namespace'] = $baseNamespace; 1956e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1957e3a9f44cSAtari911 } 1958e3a9f44cSAtari911 } 1959e3a9f44cSAtari911 } 1960e3a9f44cSAtari911 1961e3a9f44cSAtari911 // Recursively find all subdirectories 1962e3a9f44cSAtari911 $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents); 1963e3a9f44cSAtari911 1964e3a9f44cSAtari911 return $allEvents; 1965e3a9f44cSAtari911 } 1966e3a9f44cSAtari911 1967e3a9f44cSAtari911 private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) { 1968e3a9f44cSAtari911 if (!is_dir($dir)) return; 1969e3a9f44cSAtari911 1970e3a9f44cSAtari911 $items = scandir($dir); 1971e3a9f44cSAtari911 foreach ($items as $item) { 1972e3a9f44cSAtari911 if ($item === '.' || $item === '..') continue; 1973e3a9f44cSAtari911 1974e3a9f44cSAtari911 $path = $dir . $item; 1975e3a9f44cSAtari911 if (is_dir($path) && $item !== 'calendar') { 1976e3a9f44cSAtari911 // This is a namespace directory 1977e3a9f44cSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 1978e3a9f44cSAtari911 1979e3a9f44cSAtari911 // Load events from this namespace 1980e3a9f44cSAtari911 $events = $this->loadEvents($namespace, $year, $month); 1981e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1982e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1983e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1984e3a9f44cSAtari911 } 1985e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1986e3a9f44cSAtari911 $event['_namespace'] = $namespace; 1987e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1988e3a9f44cSAtari911 } 1989e3a9f44cSAtari911 } 1990e3a9f44cSAtari911 1991e3a9f44cSAtari911 // Recurse into subdirectories 1992e3a9f44cSAtari911 $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents); 1993e3a9f44cSAtari911 } 1994e3a9f44cSAtari911 } 1995e3a9f44cSAtari911 } 19961d05cddcSAtari911 19971d05cddcSAtari911 private function getAllNamespaces() { 19981d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 19991d05cddcSAtari911 $namespaces = []; 20001d05cddcSAtari911 20011d05cddcSAtari911 // Scan for namespaces that have calendar data 20021d05cddcSAtari911 $this->scanForCalendarNamespaces($dataDir, '', $namespaces); 20031d05cddcSAtari911 20041d05cddcSAtari911 // Sort alphabetically 20051d05cddcSAtari911 sort($namespaces); 20061d05cddcSAtari911 20071d05cddcSAtari911 return $namespaces; 20081d05cddcSAtari911 } 20091d05cddcSAtari911 20101d05cddcSAtari911 private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) { 20111d05cddcSAtari911 if (!is_dir($dir)) return; 20121d05cddcSAtari911 20131d05cddcSAtari911 $items = scandir($dir); 20141d05cddcSAtari911 foreach ($items as $item) { 20151d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 20161d05cddcSAtari911 20171d05cddcSAtari911 $path = $dir . $item; 20181d05cddcSAtari911 if (is_dir($path)) { 20191d05cddcSAtari911 // Check if this directory has a calendar subdirectory with data 20201d05cddcSAtari911 $calendarDir = $path . '/calendar/'; 20211d05cddcSAtari911 if (is_dir($calendarDir)) { 20221d05cddcSAtari911 // Check if there are any JSON files in the calendar directory 20231d05cddcSAtari911 $jsonFiles = glob($calendarDir . '*.json'); 20241d05cddcSAtari911 if (!empty($jsonFiles)) { 20251d05cddcSAtari911 // This namespace has calendar data 20261d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 20271d05cddcSAtari911 $namespaces[] = $namespace; 20281d05cddcSAtari911 } 20291d05cddcSAtari911 } 20301d05cddcSAtari911 20311d05cddcSAtari911 // Recurse into subdirectories 20321d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 20331d05cddcSAtari911 $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces); 20341d05cddcSAtari911 } 20351d05cddcSAtari911 } 20361d05cddcSAtari911 } 20371d05cddcSAtari911 20381d05cddcSAtari911 /** 20391d05cddcSAtari911 * Render new sidebar widget - Week at a glance itinerary (200px wide) 20401d05cddcSAtari911 */ 2041*0c3b6e81SAtari911 private function renderSidebarWidget($events, $namespace, $calId, $themeOverride = null) { 20421d05cddcSAtari911 if (empty($events)) { 20431d05cddcSAtari911 return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">No events this week</div>'; 20441d05cddcSAtari911 } 20451d05cddcSAtari911 20461d05cddcSAtari911 // Get important namespaces from config 20471d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 20481d05cddcSAtari911 $importantNsList = ['important']; // default 20491d05cddcSAtari911 if (file_exists($configFile)) { 20501d05cddcSAtari911 $config = include $configFile; 20511d05cddcSAtari911 if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) { 20521d05cddcSAtari911 $importantNsList = array_map('trim', explode(',', $config['important_namespaces'])); 20531d05cddcSAtari911 } 20541d05cddcSAtari911 } 20551d05cddcSAtari911 20561d05cddcSAtari911 // Calculate date ranges 20571d05cddcSAtari911 $todayStr = date('Y-m-d'); 20581d05cddcSAtari911 $tomorrowStr = date('Y-m-d', strtotime('+1 day')); 20599ccd446eSAtari911 20609ccd446eSAtari911 // Get week start preference and calculate week range 20619ccd446eSAtari911 $weekStartDay = $this->getWeekStartDay(); 20629ccd446eSAtari911 20639ccd446eSAtari911 if ($weekStartDay === 'monday') { 20649ccd446eSAtari911 // Monday start 20651d05cddcSAtari911 $weekStart = date('Y-m-d', strtotime('monday this week')); 20661d05cddcSAtari911 $weekEnd = date('Y-m-d', strtotime('sunday this week')); 20679ccd446eSAtari911 } else { 20689ccd446eSAtari911 // Sunday start (default - US/Canada standard) 20699ccd446eSAtari911 $today = date('w'); // 0 (Sun) to 6 (Sat) 20709ccd446eSAtari911 if ($today == 0) { 20719ccd446eSAtari911 // Today is Sunday 20729ccd446eSAtari911 $weekStart = date('Y-m-d'); 20739ccd446eSAtari911 } else { 20749ccd446eSAtari911 // Monday-Saturday: go back to last Sunday 20759ccd446eSAtari911 $weekStart = date('Y-m-d', strtotime('-' . $today . ' days')); 20769ccd446eSAtari911 } 20779ccd446eSAtari911 $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days')); 20789ccd446eSAtari911 } 20791d05cddcSAtari911 20801d05cddcSAtari911 // Group events by category 20811d05cddcSAtari911 $todayEvents = []; 20821d05cddcSAtari911 $tomorrowEvents = []; 20831d05cddcSAtari911 $importantEvents = []; 20841d05cddcSAtari911 $weekEvents = []; // For week grid 20851d05cddcSAtari911 20861d05cddcSAtari911 // Process all events 20871d05cddcSAtari911 foreach ($events as $dateKey => $dayEvents) { 20889ccd446eSAtari911 // Detect conflicts for events on this day 20899ccd446eSAtari911 $eventsWithConflicts = $this->detectTimeConflicts($dayEvents); 20901d05cddcSAtari911 20919ccd446eSAtari911 foreach ($eventsWithConflicts as $event) { 20929ccd446eSAtari911 // Always categorize Today and Tomorrow regardless of week boundaries 20939ccd446eSAtari911 if ($dateKey === $todayStr) { 20949ccd446eSAtari911 $todayEvents[] = array_merge($event, ['date' => $dateKey]); 20959ccd446eSAtari911 } 20969ccd446eSAtari911 if ($dateKey === $tomorrowStr) { 20979ccd446eSAtari911 $tomorrowEvents[] = array_merge($event, ['date' => $dateKey]); 20989ccd446eSAtari911 } 20999ccd446eSAtari911 21009ccd446eSAtari911 // Process week grid events (only for current week) 21011d05cddcSAtari911 if ($dateKey >= $weekStart && $dateKey <= $weekEnd) { 21029ccd446eSAtari911 // Initialize week grid day if not exists 21031d05cddcSAtari911 if (!isset($weekEvents[$dateKey])) { 21041d05cddcSAtari911 $weekEvents[$dateKey] = []; 21051d05cddcSAtari911 } 21061d05cddcSAtari911 21071d05cddcSAtari911 // Pre-render DokuWiki syntax to HTML for JavaScript display 21081d05cddcSAtari911 $eventWithHtml = $event; 21091d05cddcSAtari911 if (isset($event['title'])) { 21101d05cddcSAtari911 $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']); 21111d05cddcSAtari911 } 21121d05cddcSAtari911 if (isset($event['description'])) { 21131d05cddcSAtari911 $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']); 21141d05cddcSAtari911 } 21151d05cddcSAtari911 $weekEvents[$dateKey][] = $eventWithHtml; 21161d05cddcSAtari911 } 21171d05cddcSAtari911 21181d05cddcSAtari911 // Check if this is an important namespace 21191d05cddcSAtari911 $eventNs = isset($event['namespace']) ? $event['namespace'] : ''; 21201d05cddcSAtari911 $isImportant = false; 21211d05cddcSAtari911 foreach ($importantNsList as $impNs) { 21221d05cddcSAtari911 if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) { 21231d05cddcSAtari911 $isImportant = true; 21241d05cddcSAtari911 break; 21251d05cddcSAtari911 } 21261d05cddcSAtari911 } 21271d05cddcSAtari911 21289ccd446eSAtari911 // Important events: show from today through next 2 weeks 21299ccd446eSAtari911 if ($isImportant && $dateKey >= $todayStr) { 21301d05cddcSAtari911 $importantEvents[] = array_merge($event, ['date' => $dateKey]); 21311d05cddcSAtari911 } 21321d05cddcSAtari911 } 21331d05cddcSAtari911 } 21349ccd446eSAtari911 21359ccd446eSAtari911 // Sort Important Events by date (earliest first) 21369ccd446eSAtari911 usort($importantEvents, function($a, $b) { 21379ccd446eSAtari911 $dateA = isset($a['date']) ? $a['date'] : ''; 21389ccd446eSAtari911 $dateB = isset($b['date']) ? $b['date'] : ''; 21399ccd446eSAtari911 21409ccd446eSAtari911 // Compare dates 21419ccd446eSAtari911 if ($dateA === $dateB) { 21429ccd446eSAtari911 // Same date - sort by time 21439ccd446eSAtari911 $timeA = isset($a['time']) ? $a['time'] : ''; 21449ccd446eSAtari911 $timeB = isset($b['time']) ? $b['time'] : ''; 21459ccd446eSAtari911 21469ccd446eSAtari911 if (empty($timeA) && !empty($timeB)) return 1; // All-day events last 21479ccd446eSAtari911 if (!empty($timeA) && empty($timeB)) return -1; 21489ccd446eSAtari911 if (empty($timeA) && empty($timeB)) return 0; 21499ccd446eSAtari911 21509ccd446eSAtari911 // Both have times 21519ccd446eSAtari911 $aMinutes = $this->timeToMinutes($timeA); 21529ccd446eSAtari911 $bMinutes = $this->timeToMinutes($timeB); 21539ccd446eSAtari911 return $aMinutes - $bMinutes; 21541d05cddcSAtari911 } 21551d05cddcSAtari911 21569ccd446eSAtari911 return strcmp($dateA, $dateB); 21579ccd446eSAtari911 }); 21589ccd446eSAtari911 2159*0c3b6e81SAtari911 // Get theme - prefer override from syntax parameter, fall back to admin default 2160*0c3b6e81SAtari911 $theme = !empty($themeOverride) ? $themeOverride : $this->getSidebarTheme(); 21619ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 21629ccd446eSAtari911 $themeClass = 'sidebar-' . $theme; 21639ccd446eSAtari911 21649ccd446eSAtari911 // Start building HTML - Dynamic width with default font (overflow:visible for tooltips) 21659ccd446eSAtari911 $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;">'; 21669ccd446eSAtari911 21679ccd446eSAtari911 // Inject CSS variables so the event dialog (shared component) picks up the theme 21689ccd446eSAtari911 $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg']; 21699ccd446eSAtari911 $html .= '<style> 21709ccd446eSAtari911 #sidebar-widget-' . $calId . ' { 21719ccd446eSAtari911 --background-site: ' . $themeStyles['bg'] . '; 21729ccd446eSAtari911 --background-alt: ' . $themeStyles['cell_bg'] . '; 21739ccd446eSAtari911 --background-header: ' . $themeStyles['header_bg'] . '; 21749ccd446eSAtari911 --text-primary: ' . $themeStyles['text_primary'] . '; 21759ccd446eSAtari911 --text-dim: ' . $themeStyles['text_dim'] . '; 21769ccd446eSAtari911 --text-bright: ' . $themeStyles['text_bright'] . '; 21779ccd446eSAtari911 --border-color: ' . $themeStyles['grid_border'] . '; 21789ccd446eSAtari911 --border-main: ' . $themeStyles['border'] . '; 21799ccd446eSAtari911 --cell-bg: ' . $themeStyles['cell_bg'] . '; 21809ccd446eSAtari911 --cell-today-bg: ' . $themeStyles['cell_today_bg'] . '; 21819ccd446eSAtari911 --shadow-color: ' . $themeStyles['shadow'] . '; 21829ccd446eSAtari911 --header-border: ' . $themeStyles['header_border'] . '; 21839ccd446eSAtari911 --header-shadow: ' . $themeStyles['header_shadow'] . '; 21849ccd446eSAtari911 --grid-bg: ' . $themeStyles['grid_bg'] . '; 21859ccd446eSAtari911 --btn-text: ' . $btnTextColor . '; 21869ccd446eSAtari911 } 21879ccd446eSAtari911 </style>'; 21889ccd446eSAtari911 21899ccd446eSAtari911 // Add sparkle effect for pink theme 21909ccd446eSAtari911 if ($theme === 'pink') { 21919ccd446eSAtari911 $html .= '<style> 21929ccd446eSAtari911 @keyframes sparkle-' . $calId . ' { 21939ccd446eSAtari911 0% { 21949ccd446eSAtari911 opacity: 0; 21959ccd446eSAtari911 transform: translate(0, 0) scale(0) rotate(0deg); 21969ccd446eSAtari911 } 21979ccd446eSAtari911 50% { 21989ccd446eSAtari911 opacity: 1; 21999ccd446eSAtari911 transform: translate(var(--tx), var(--ty)) scale(1) rotate(180deg); 22009ccd446eSAtari911 } 22019ccd446eSAtari911 100% { 22029ccd446eSAtari911 opacity: 0; 22039ccd446eSAtari911 transform: translate(calc(var(--tx) * 2), calc(var(--ty) * 2)) scale(0) rotate(360deg); 22049ccd446eSAtari911 } 22059ccd446eSAtari911 } 22069ccd446eSAtari911 22079ccd446eSAtari911 @keyframes pulse-glow-' . $calId . ' { 22089ccd446eSAtari911 0%, 100% { box-shadow: 0 0 10px rgba(255, 20, 147, 0.4); } 22099ccd446eSAtari911 50% { box-shadow: 0 0 25px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4); } 22109ccd446eSAtari911 } 22119ccd446eSAtari911 22129ccd446eSAtari911 @keyframes shimmer-' . $calId . ' { 22139ccd446eSAtari911 0% { background-position: -200% center; } 22149ccd446eSAtari911 100% { background-position: 200% center; } 22159ccd446eSAtari911 } 22169ccd446eSAtari911 22179ccd446eSAtari911 .sidebar-pink { 22189ccd446eSAtari911 animation: pulse-glow-' . $calId . ' 3s ease-in-out infinite; 22199ccd446eSAtari911 } 22209ccd446eSAtari911 22219ccd446eSAtari911 .sidebar-pink:hover { 22229ccd446eSAtari911 box-shadow: 0 0 30px rgba(255, 20, 147, 0.9), 0 0 50px rgba(255, 20, 147, 0.5) !important; 22239ccd446eSAtari911 } 22249ccd446eSAtari911 22259ccd446eSAtari911 .sparkle-' . $calId . ' { 22269ccd446eSAtari911 position: absolute; 22279ccd446eSAtari911 pointer-events: none; 22289ccd446eSAtari911 font-size: 20px; 22299ccd446eSAtari911 z-index: 1000; 22309ccd446eSAtari911 animation: sparkle-' . $calId . ' 1s ease-out forwards; 22319ccd446eSAtari911 filter: drop-shadow(0 0 3px rgba(255, 20, 147, 0.8)); 22329ccd446eSAtari911 } 22339ccd446eSAtari911 </style>'; 22349ccd446eSAtari911 22359ccd446eSAtari911 $html .= '<script> 22369ccd446eSAtari911 (function() { 22379ccd446eSAtari911 const container = document.getElementById("sidebar-widget-' . $calId . '"); 22389ccd446eSAtari911 const sparkles = ["✨", "", "", "⭐", "", "", "", "", "", ""]; 22399ccd446eSAtari911 22409ccd446eSAtari911 function createSparkle(x, y) { 22419ccd446eSAtari911 const sparkle = document.createElement("div"); 22429ccd446eSAtari911 sparkle.className = "sparkle-' . $calId . '"; 22439ccd446eSAtari911 sparkle.textContent = sparkles[Math.floor(Math.random() * sparkles.length)]; 22449ccd446eSAtari911 sparkle.style.left = x + "px"; 22459ccd446eSAtari911 sparkle.style.top = y + "px"; 22469ccd446eSAtari911 22479ccd446eSAtari911 // Random direction 22489ccd446eSAtari911 const angle = Math.random() * Math.PI * 2; 22499ccd446eSAtari911 const distance = 30 + Math.random() * 40; 22509ccd446eSAtari911 sparkle.style.setProperty("--tx", Math.cos(angle) * distance + "px"); 22519ccd446eSAtari911 sparkle.style.setProperty("--ty", Math.sin(angle) * distance + "px"); 22529ccd446eSAtari911 22539ccd446eSAtari911 container.appendChild(sparkle); 22549ccd446eSAtari911 22559ccd446eSAtari911 setTimeout(() => sparkle.remove(), 1000); 22569ccd446eSAtari911 } 22579ccd446eSAtari911 22589ccd446eSAtari911 // Click sparkles 22599ccd446eSAtari911 container.addEventListener("click", function(e) { 22609ccd446eSAtari911 const rect = container.getBoundingClientRect(); 22619ccd446eSAtari911 const x = e.clientX - rect.left; 22629ccd446eSAtari911 const y = e.clientY - rect.top; 22639ccd446eSAtari911 22649ccd446eSAtari911 // Create LOTS of sparkles for maximum bling! 22659ccd446eSAtari911 for (let i = 0; i < 8; i++) { 22669ccd446eSAtari911 setTimeout(() => { 22679ccd446eSAtari911 const offsetX = x + (Math.random() - 0.5) * 30; 22689ccd446eSAtari911 const offsetY = y + (Math.random() - 0.5) * 30; 22699ccd446eSAtari911 createSparkle(offsetX, offsetY); 22709ccd446eSAtari911 }, i * 40); 22719ccd446eSAtari911 } 22729ccd446eSAtari911 }); 22739ccd446eSAtari911 22749ccd446eSAtari911 // Random auto-sparkles for extra glamour 22759ccd446eSAtari911 setInterval(() => { 22769ccd446eSAtari911 const x = Math.random() * container.offsetWidth; 22779ccd446eSAtari911 const y = Math.random() * container.offsetHeight; 22789ccd446eSAtari911 createSparkle(x, y); 22799ccd446eSAtari911 }, 3000); 22809ccd446eSAtari911 })(); 22819ccd446eSAtari911 </script>'; 22829ccd446eSAtari911 } 22831d05cddcSAtari911 22841d05cddcSAtari911 // Sanitize calId for use in JavaScript variable names (remove dashes) 22851d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); 22861d05cddcSAtari911 22871d05cddcSAtari911 // CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it 22881d05cddcSAtari911 $html .= '<script> 22891d05cddcSAtari911(function() { 22901d05cddcSAtari911 // Shared state for system stats and tooltips 22911d05cddcSAtari911 const sharedState_' . $jsCalId . ' = { 22921d05cddcSAtari911 latestStats: { 22931d05cddcSAtari911 load: {"1min": 0, "5min": 0, "15min": 0}, 22941d05cddcSAtari911 uptime: "", 22951d05cddcSAtari911 memory_details: {}, 22961d05cddcSAtari911 top_processes: [] 22971d05cddcSAtari911 }, 22981d05cddcSAtari911 cpuHistory: [], 22991d05cddcSAtari911 CPU_HISTORY_SIZE: 2 23001d05cddcSAtari911 }; 23011d05cddcSAtari911 23021d05cddcSAtari911 // Tooltip functions - MUST be defined before HTML uses them 23031d05cddcSAtari911 window["showTooltip_' . $jsCalId . '"] = function(color) { 23041d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 23051d05cddcSAtari911 if (!tooltip) { 23061d05cddcSAtari911 console.log("Tooltip element not found for color:", color); 23071d05cddcSAtari911 return; 23081d05cddcSAtari911 } 23091d05cddcSAtari911 23101d05cddcSAtari911 const latestStats = sharedState_' . $jsCalId . '.latestStats; 23111d05cddcSAtari911 let content = ""; 23121d05cddcSAtari911 23131d05cddcSAtari911 if (color === "green") { 23141d05cddcSAtari911 content = "<div class=\\"tooltip-title\\">CPU Load Average</div>"; 23151d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 23161d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 23171d05cddcSAtari911 content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>"; 23181d05cddcSAtari911 if (latestStats.uptime) { 23191d05cddcSAtari911 content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\\">Uptime: " + latestStats.uptime + "</div>"; 23201d05cddcSAtari911 } 23211d05cddcSAtari911 tooltip.style.borderColor = "#00cc07"; 23221d05cddcSAtari911 tooltip.style.color = "#00cc07"; 23231d05cddcSAtari911 } else if (color === "purple") { 23241d05cddcSAtari911 content = "<div class=\\"tooltip-title\\">CPU Load (Short-term)</div>"; 23251d05cddcSAtari911 content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>"; 23261d05cddcSAtari911 content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>"; 23271d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 23281d05cddcSAtari911 content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(155,89,182,0.3);\\" class=\\"tooltip-title\\">Top Processes</div>"; 23291d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 23301d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 23311d05cddcSAtari911 }); 23321d05cddcSAtari911 } 23331d05cddcSAtari911 tooltip.style.borderColor = "#9b59b6"; 23341d05cddcSAtari911 tooltip.style.color = "#9b59b6"; 23351d05cddcSAtari911 } else if (color === "orange") { 23361d05cddcSAtari911 content = "<div class=\\"tooltip-title\\">Memory Usage</div>"; 23371d05cddcSAtari911 if (latestStats.memory_details && latestStats.memory_details.total) { 23381d05cddcSAtari911 content += "<div>Total: " + latestStats.memory_details.total + "</div>"; 23391d05cddcSAtari911 content += "<div>Used: " + latestStats.memory_details.used + "</div>"; 23401d05cddcSAtari911 content += "<div>Available: " + latestStats.memory_details.available + "</div>"; 23411d05cddcSAtari911 if (latestStats.memory_details.cached) { 23421d05cddcSAtari911 content += "<div>Cached: " + latestStats.memory_details.cached + "</div>"; 23431d05cddcSAtari911 } 23441d05cddcSAtari911 } else { 23451d05cddcSAtari911 content += "<div>Loading...</div>"; 23461d05cddcSAtari911 } 23471d05cddcSAtari911 if (latestStats.top_processes && latestStats.top_processes.length > 0) { 23481d05cddcSAtari911 content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(255,140,0,0.3);\\" class=\\"tooltip-title\\">Top Processes</div>"; 23491d05cddcSAtari911 latestStats.top_processes.slice(0, 5).forEach(proc => { 23501d05cddcSAtari911 content += "<div>" + proc.cpu + " " + proc.command + "</div>"; 23511d05cddcSAtari911 }); 23521d05cddcSAtari911 } 23531d05cddcSAtari911 tooltip.style.borderColor = "#ff9800"; 23541d05cddcSAtari911 tooltip.style.color = "#ff9800"; 23551d05cddcSAtari911 } 23561d05cddcSAtari911 23571d05cddcSAtari911 tooltip.innerHTML = content; 23581d05cddcSAtari911 tooltip.style.display = "block"; 23591d05cddcSAtari911 23601d05cddcSAtari911 const bar = tooltip.parentElement; 23611d05cddcSAtari911 const barRect = bar.getBoundingClientRect(); 23621d05cddcSAtari911 const tooltipRect = tooltip.getBoundingClientRect(); 23631d05cddcSAtari911 23641d05cddcSAtari911 const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2); 23651d05cddcSAtari911 const top = barRect.top - tooltipRect.height - 8; 23661d05cddcSAtari911 23671d05cddcSAtari911 tooltip.style.left = left + "px"; 23681d05cddcSAtari911 tooltip.style.top = top + "px"; 23691d05cddcSAtari911 }; 23701d05cddcSAtari911 23711d05cddcSAtari911 window["hideTooltip_' . $jsCalId . '"] = function(color) { 23721d05cddcSAtari911 const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '"); 23731d05cddcSAtari911 if (tooltip) { 23741d05cddcSAtari911 tooltip.style.display = "none"; 23751d05cddcSAtari911 } 23761d05cddcSAtari911 }; 23771d05cddcSAtari911 23781d05cddcSAtari911 // Update clock every second 23791d05cddcSAtari911 function updateClock() { 23801d05cddcSAtari911 const now = new Date(); 23811d05cddcSAtari911 let hours = now.getHours(); 23821d05cddcSAtari911 const minutes = String(now.getMinutes()).padStart(2, "0"); 23831d05cddcSAtari911 const seconds = String(now.getSeconds()).padStart(2, "0"); 23841d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 23851d05cddcSAtari911 hours = hours % 12 || 12; 23861d05cddcSAtari911 const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm; 23871d05cddcSAtari911 const clockEl = document.getElementById("clock-' . $calId . '"); 23881d05cddcSAtari911 if (clockEl) clockEl.textContent = timeStr; 23891d05cddcSAtari911 } 23901d05cddcSAtari911 setInterval(updateClock, 1000); 23911d05cddcSAtari911 23921d05cddcSAtari911 // Weather update function 23931d05cddcSAtari911 function updateWeather() { 23941d05cddcSAtari911 if ("geolocation" in navigator) { 23951d05cddcSAtari911 navigator.geolocation.getCurrentPosition(function(position) { 23961d05cddcSAtari911 const lat = position.coords.latitude; 23971d05cddcSAtari911 const lon = position.coords.longitude; 23981d05cddcSAtari911 23991d05cddcSAtari911 fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true&temperature_unit=fahrenheit`) 24001d05cddcSAtari911 .then(response => response.json()) 24011d05cddcSAtari911 .then(data => { 24021d05cddcSAtari911 if (data.current_weather) { 24031d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 24041d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 24051d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 24061d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 24071d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 24081d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 24091d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 24101d05cddcSAtari911 } 24111d05cddcSAtari911 }) 24121d05cddcSAtari911 .catch(error => console.log("Weather fetch error:", error)); 24131d05cddcSAtari911 }, function(error) { 24141d05cddcSAtari911 // If geolocation fails, use default location (Irvine, CA) 24151d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265¤t_weather=true&temperature_unit=fahrenheit") 24161d05cddcSAtari911 .then(response => response.json()) 24171d05cddcSAtari911 .then(data => { 24181d05cddcSAtari911 if (data.current_weather) { 24191d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 24201d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 24211d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 24221d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 24231d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 24241d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 24251d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 24261d05cddcSAtari911 } 24271d05cddcSAtari911 }) 24281d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 24291d05cddcSAtari911 }); 24301d05cddcSAtari911 } else { 24311d05cddcSAtari911 // No geolocation, use default (Irvine, CA) 24321d05cddcSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265¤t_weather=true&temperature_unit=fahrenheit") 24331d05cddcSAtari911 .then(response => response.json()) 24341d05cddcSAtari911 .then(data => { 24351d05cddcSAtari911 if (data.current_weather) { 24361d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 24371d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 24381d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 24391d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 24401d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 24411d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 24421d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 24431d05cddcSAtari911 } 24441d05cddcSAtari911 }) 24451d05cddcSAtari911 .catch(err => console.log("Weather error:", err)); 24461d05cddcSAtari911 } 24471d05cddcSAtari911 } 24481d05cddcSAtari911 24491d05cddcSAtari911 function getWeatherIcon(code) { 24501d05cddcSAtari911 const icons = { 24511d05cddcSAtari911 0: "☀️", 1: "️", 2: "⛅", 3: "☁️", 24521d05cddcSAtari911 45: "️", 48: "️", 51: "️", 53: "️", 55: "️", 24531d05cddcSAtari911 61: "️", 63: "️", 65: "⛈️", 71: "️", 73: "️", 24541d05cddcSAtari911 75: "❄️", 77: "️", 80: "️", 81: "️", 82: "⛈️", 24551d05cddcSAtari911 85: "️", 86: "❄️", 95: "⛈️", 96: "⛈️", 99: "⛈️" 24561d05cddcSAtari911 }; 24571d05cddcSAtari911 return icons[code] || "️"; 24581d05cddcSAtari911 } 24591d05cddcSAtari911 24601d05cddcSAtari911 // Update weather immediately and every 10 minutes 24611d05cddcSAtari911 updateWeather(); 24621d05cddcSAtari911 setInterval(updateWeather, 600000); 24631d05cddcSAtari911 24641d05cddcSAtari911 // Update system stats and tooltips data 24651d05cddcSAtari911 function updateSystemStats() { 24661d05cddcSAtari911 fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php") 24671d05cddcSAtari911 .then(response => response.json()) 24681d05cddcSAtari911 .then(data => { 24691d05cddcSAtari911 sharedState_' . $jsCalId . '.latestStats = { 24701d05cddcSAtari911 load: data.load || {"1min": 0, "5min": 0, "15min": 0}, 24711d05cddcSAtari911 uptime: data.uptime || "", 24721d05cddcSAtari911 memory_details: data.memory_details || {}, 24731d05cddcSAtari911 top_processes: data.top_processes || [] 24741d05cddcSAtari911 }; 24751d05cddcSAtari911 24761d05cddcSAtari911 const greenBar = document.getElementById("cpu-5min-' . $calId . '"); 24771d05cddcSAtari911 if (greenBar) { 24781d05cddcSAtari911 greenBar.style.width = Math.min(100, data.cpu_5min) + "%"; 24791d05cddcSAtari911 } 24801d05cddcSAtari911 24811d05cddcSAtari911 sharedState_' . $jsCalId . '.cpuHistory.push(data.cpu); 24821d05cddcSAtari911 if (sharedState_' . $jsCalId . '.cpuHistory.length > sharedState_' . $jsCalId . '.CPU_HISTORY_SIZE) { 24831d05cddcSAtari911 sharedState_' . $jsCalId . '.cpuHistory.shift(); 24841d05cddcSAtari911 } 24851d05cddcSAtari911 24861d05cddcSAtari911 const cpuAverage = sharedState_' . $jsCalId . '.cpuHistory.reduce((sum, val) => sum + val, 0) / sharedState_' . $jsCalId . '.cpuHistory.length; 24871d05cddcSAtari911 24881d05cddcSAtari911 const cpuBar = document.getElementById("cpu-realtime-' . $calId . '"); 24891d05cddcSAtari911 if (cpuBar) { 24901d05cddcSAtari911 cpuBar.style.width = Math.min(100, cpuAverage) + "%"; 24911d05cddcSAtari911 } 24921d05cddcSAtari911 24931d05cddcSAtari911 const memBar = document.getElementById("mem-realtime-' . $calId . '"); 24941d05cddcSAtari911 if (memBar) { 24951d05cddcSAtari911 memBar.style.width = Math.min(100, data.memory) + "%"; 24961d05cddcSAtari911 } 24971d05cddcSAtari911 }) 24981d05cddcSAtari911 .catch(error => { 24991d05cddcSAtari911 console.log("System stats error:", error); 25001d05cddcSAtari911 }); 25011d05cddcSAtari911 } 25021d05cddcSAtari911 25031d05cddcSAtari911 updateSystemStats(); 25041d05cddcSAtari911 setInterval(updateSystemStats, 2000); 25051d05cddcSAtari911})(); 25061d05cddcSAtari911</script>'; 25071d05cddcSAtari911 25081d05cddcSAtari911 // NOW add the header HTML (after JavaScript is defined) 25091d05cddcSAtari911 $todayDate = new DateTime(); 25101d05cddcSAtari911 $displayDate = $todayDate->format('D, M j, Y'); 25111d05cddcSAtari911 $currentTime = $todayDate->format('g:i:s A'); 25121d05cddcSAtari911 25139ccd446eSAtari911 $html .= '<div class="eventlist-today-header" style="background:' . $themeStyles['header_bg'] . '; border:2px solid ' . $themeStyles['header_border'] . '; box-shadow:' . $themeStyles['header_shadow'] . ';">'; 25149ccd446eSAtari911 $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '" style="color:' . $themeStyles['text_bright'] . ';">' . $currentTime . '</span>'; 25151d05cddcSAtari911 $html .= '<div class="eventlist-bottom-info">'; 25169ccd446eSAtari911 $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">️</span> <span id="weather-temp-' . $calId . '" style="color:' . $themeStyles['text_primary'] . ';">--°</span></span>'; 25179ccd446eSAtari911 $html .= '<span class="eventlist-today-date" style="color:' . $themeStyles['text_dim'] . ';">' . $displayDate . '</span>'; 25181d05cddcSAtari911 $html .= '</div>'; 25191d05cddcSAtari911 25201d05cddcSAtari911 // Three CPU/Memory bars (all update live) 25211d05cddcSAtari911 $html .= '<div class="eventlist-stats-container">'; 25221d05cddcSAtari911 25231d05cddcSAtari911 // 5-minute load average (green, updates every 2 seconds) 25241d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $jsCalId . '(\'green\')" onmouseout="hideTooltip_' . $jsCalId . '(\'green\')">'; 25251d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>'; 25261d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>'; 25271d05cddcSAtari911 $html .= '</div>'; 25281d05cddcSAtari911 25291d05cddcSAtari911 // Real-time CPU (purple, updates with 5-sec average) 25301d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'purple\')" onmouseout="hideTooltip_' . $jsCalId . '(\'purple\')">'; 25311d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>'; 25321d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>'; 25331d05cddcSAtari911 $html .= '</div>'; 25341d05cddcSAtari911 25351d05cddcSAtari911 // Real-time Memory (orange, updates) 25361d05cddcSAtari911 $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'orange\')" onmouseout="hideTooltip_' . $jsCalId . '(\'orange\')">'; 25371d05cddcSAtari911 $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>'; 25381d05cddcSAtari911 $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>'; 25391d05cddcSAtari911 $html .= '</div>'; 25401d05cddcSAtari911 25411d05cddcSAtari911 $html .= '</div>'; 25421d05cddcSAtari911 $html .= '</div>'; 25431d05cddcSAtari911 2544231d0edbSAtari911 // Get today's date for default event date 2545231d0edbSAtari911 $todayStr = date('Y-m-d'); 2546231d0edbSAtari911 25479ccd446eSAtari911 // Thin "Add Event" bar between header and week grid - theme-aware colors 25489ccd446eSAtari911 $addBtnBg = $theme === 'matrix' ? '#006400' : 25499ccd446eSAtari911 ($theme === 'purple' ? '#7d3c98' : 25509ccd446eSAtari911 ($theme === 'pink' ? '#b8156f' : 25519ccd446eSAtari911 ($theme === 'wiki' ? $themeStyles['grid_bg'] : '#3498db'))); 25529ccd446eSAtari911 $addBtnHover = $theme === 'matrix' ? '#004d00' : 25539ccd446eSAtari911 ($theme === 'purple' ? '#5b2c6f' : 25549ccd446eSAtari911 ($theme === 'pink' ? '#8b0f54' : 25559ccd446eSAtari911 ($theme === 'wiki' ? $themeStyles['cell_today_bg'] : '#2980b9'))); 25569ccd446eSAtari911 $addBtnTextColor = $theme === 'professional' ? '#ffffff' : 25579ccd446eSAtari911 ($theme === 'wiki' ? $themeStyles['text_primary'] : 25589ccd446eSAtari911 ($theme === 'pink' ? '#000000' : $themeStyles['text_bright'])); 25599ccd446eSAtari911 ($theme === 'pink' ? '#000000' : $themeStyles['text_bright']); 25609ccd446eSAtari911 $addBtnShadow = $theme === 'matrix' ? '0 0 8px rgba(0, 100, 0, 0.4)' : 25619ccd446eSAtari911 ($theme === 'purple' ? '0 0 8px rgba(155, 89, 182, 0.4)' : 25629ccd446eSAtari911 ($theme === 'pink' ? '0 0 10px rgba(255, 20, 147, 0.5)' : '0 2px 4px rgba(0,0,0,0.2)')); 25639ccd446eSAtari911 $addBtnHoverShadow = $theme === 'matrix' ? '0 0 12px rgba(0, 100, 0, 0.6)' : 25649ccd446eSAtari911 ($theme === 'purple' ? '0 0 12px rgba(155, 89, 182, 0.6)' : 25659ccd446eSAtari911 ($theme === 'pink' ? '0 0 14px rgba(255, 20, 147, 0.7)' : '0 3px 6px rgba(0,0,0,0.3)')); 25669ccd446eSAtari911 25679ccd446eSAtari911 $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 . '\';">'; 25689ccd446eSAtari911 $addBtnTextShadow = ($theme === 'pink') ? '0 0 3px ' . $addBtnTextColor : 'none'; 25699ccd446eSAtari911 $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>'; 25701d05cddcSAtari911 $html .= '</div>'; 25711d05cddcSAtari911 25721d05cddcSAtari911 // Week grid (7 cells) 25739ccd446eSAtari911 $html .= $this->renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme); 25741d05cddcSAtari911 25759ccd446eSAtari911 // Section colors - different shades for pink theme, template colors for wiki 25769ccd446eSAtari911 if ($theme === 'wiki') { 25779ccd446eSAtari911 $todayColor = '#e67e22'; // Warm orange - stands out on light bg 25789ccd446eSAtari911 $tomorrowColor = '#27ae60'; // Green - distinct from orange 25799ccd446eSAtari911 $importantColor = '#8e44ad'; // Purple - distinct from both 25809ccd446eSAtari911 } else { 25819ccd446eSAtari911 $todayColor = $theme === 'pink' ? '#ff1493' : '#ff9800'; // Hot pink vs orange 25829ccd446eSAtari911 $tomorrowColor = $theme === 'pink' ? '#ff69b4' : '#4caf50'; // Pink vs green 25839ccd446eSAtari911 $importantColor = $theme === 'pink' ? '#ff85c1' : '#9b59b6'; // Light pink vs purple 25849ccd446eSAtari911 } 25859ccd446eSAtari911 25869ccd446eSAtari911 // Today section 25871d05cddcSAtari911 if (!empty($todayEvents)) { 25889ccd446eSAtari911 $html .= $this->renderSidebarSection('Today', $todayEvents, $todayColor, $calId, $themeStyles, $theme); 25891d05cddcSAtari911 } 25901d05cddcSAtari911 25919ccd446eSAtari911 // Tomorrow section 25921d05cddcSAtari911 if (!empty($tomorrowEvents)) { 25939ccd446eSAtari911 $html .= $this->renderSidebarSection('Tomorrow', $tomorrowEvents, $tomorrowColor, $calId, $themeStyles, $theme); 25941d05cddcSAtari911 } 25951d05cddcSAtari911 25969ccd446eSAtari911 // Important events section 25971d05cddcSAtari911 if (!empty($importantEvents)) { 25989ccd446eSAtari911 $html .= $this->renderSidebarSection('Important Events', $importantEvents, $importantColor, $calId, $themeStyles, $theme); 25991d05cddcSAtari911 } 26001d05cddcSAtari911 26011d05cddcSAtari911 $html .= '</div>'; 26021d05cddcSAtari911 2603231d0edbSAtari911 // Add event dialog for sidebar widget 2604*0c3b6e81SAtari911 $html .= $this->renderEventDialog($calId, $namespace, $theme); 2605231d0edbSAtari911 26069ccd446eSAtari911 // Add JavaScript for positioning data-tooltip elements 26079ccd446eSAtari911 $html .= '<script> 26089ccd446eSAtari911 // Position data-tooltip elements to prevent cutoff (up and to the LEFT) 26099ccd446eSAtari911 document.addEventListener("DOMContentLoaded", function() { 26109ccd446eSAtari911 const tooltipElements = document.querySelectorAll("[data-tooltip]"); 26119ccd446eSAtari911 const isPinkTheme = document.querySelector(".sidebar-pink") !== null; 26129ccd446eSAtari911 26139ccd446eSAtari911 tooltipElements.forEach(function(element) { 26149ccd446eSAtari911 element.addEventListener("mouseenter", function() { 26159ccd446eSAtari911 const rect = element.getBoundingClientRect(); 26169ccd446eSAtari911 const style = window.getComputedStyle(element, ":before"); 26179ccd446eSAtari911 26189ccd446eSAtari911 // Position above the element, aligned to LEFT (not right) 26199ccd446eSAtari911 element.style.setProperty("--tooltip-left", (rect.left - 150) + "px"); 26209ccd446eSAtari911 element.style.setProperty("--tooltip-top", (rect.top - 30) + "px"); 26219ccd446eSAtari911 26229ccd446eSAtari911 // Pink theme: position heart to the right of tooltip 26239ccd446eSAtari911 if (isPinkTheme) { 26249ccd446eSAtari911 element.style.setProperty("--heart-left", (rect.left - 150 + 210) + "px"); 26259ccd446eSAtari911 element.style.setProperty("--heart-top", (rect.top - 30) + "px"); 26269ccd446eSAtari911 } 26279ccd446eSAtari911 }); 26289ccd446eSAtari911 }); 26299ccd446eSAtari911 }); 26309ccd446eSAtari911 26319ccd446eSAtari911 // Apply custom properties to position tooltips 26329ccd446eSAtari911 const style = document.createElement("style"); 26339ccd446eSAtari911 style.textContent = ` 26349ccd446eSAtari911 [data-tooltip]:hover:before { 26359ccd446eSAtari911 left: var(--tooltip-left, 0) !important; 26369ccd446eSAtari911 top: var(--tooltip-top, 0) !important; 26379ccd446eSAtari911 } 26389ccd446eSAtari911 .sidebar-pink [data-tooltip]:hover:after { 26399ccd446eSAtari911 left: var(--heart-left, 0) !important; 26409ccd446eSAtari911 top: var(--heart-top, 0) !important; 26419ccd446eSAtari911 } 26429ccd446eSAtari911 `; 26439ccd446eSAtari911 document.head.appendChild(style); 26449ccd446eSAtari911 </script>'; 26459ccd446eSAtari911 26461d05cddcSAtari911 return $html; 26471d05cddcSAtari911 } 26481d05cddcSAtari911 26491d05cddcSAtari911 /** 26509ccd446eSAtari911 * Render compact week grid (7 cells with event bars) - Theme-aware 26511d05cddcSAtari911 */ 26529ccd446eSAtari911 private function renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme) { 26531d05cddcSAtari911 // Generate unique ID for this calendar instance - sanitize for JavaScript 26541d05cddcSAtari911 $calId = 'cal_' . substr(md5($weekStart . microtime()), 0, 8); 26551d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); // Sanitize for JS variable names 26561d05cddcSAtari911 26579ccd446eSAtari911 $html = '<div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:' . $themeStyles['grid_bg'] . '; border-bottom:2px solid ' . $themeStyles['grid_border'] . ';">'; 26581d05cddcSAtari911 26599ccd446eSAtari911 // Day names depend on week start setting 26609ccd446eSAtari911 $weekStartDay = $this->getWeekStartDay(); 26619ccd446eSAtari911 if ($weekStartDay === 'monday') { 26629ccd446eSAtari911 $dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; // Monday to Sunday 26639ccd446eSAtari911 } else { 26649ccd446eSAtari911 $dayNames = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; // Sunday to Saturday 26659ccd446eSAtari911 } 26661d05cddcSAtari911 $today = date('Y-m-d'); 26671d05cddcSAtari911 26681d05cddcSAtari911 for ($i = 0; $i < 7; $i++) { 26691d05cddcSAtari911 $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days')); 26701d05cddcSAtari911 $dayNum = date('j', strtotime($date)); 26711d05cddcSAtari911 $isToday = $date === $today; 26721d05cddcSAtari911 26731d05cddcSAtari911 $events = isset($weekEvents[$date]) ? $weekEvents[$date] : []; 26741d05cddcSAtari911 $eventCount = count($events); 26751d05cddcSAtari911 26769ccd446eSAtari911 $bgColor = $isToday ? $themeStyles['cell_today_bg'] : $themeStyles['cell_bg']; 26779ccd446eSAtari911 $textColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary']; 26781d05cddcSAtari911 $fontWeight = $isToday ? '700' : '500'; 26799ccd446eSAtari911 26809ccd446eSAtari911 // Theme-aware text shadow 26819ccd446eSAtari911 if ($theme === 'pink') { 26829ccd446eSAtari911 $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary']; 26839ccd446eSAtari911 $textShadow = $isToday ? 'text-shadow:0 0 6px ' . $glowColor . ';' : 'text-shadow:0 0 4px ' . $glowColor . ';'; 26849ccd446eSAtari911 } else { 26859ccd446eSAtari911 $textShadow = ''; // No glow for other themes 26869ccd446eSAtari911 } 26879ccd446eSAtari911 26889ccd446eSAtari911 // Border color based on theme 26899ccd446eSAtari911 $borderColor = $themeStyles['grid_border']; 26901d05cddcSAtari911 26911d05cddcSAtari911 $hasEvents = $eventCount > 0; 26921d05cddcSAtari911 $clickableStyle = $hasEvents ? 'cursor:pointer;' : ''; 26931d05cddcSAtari911 $clickHandler = $hasEvents ? ' onclick="showDayEvents_' . $jsCalId . '(\'' . $date . '\')"' : ''; 26941d05cddcSAtari911 26959ccd446eSAtari911 $html .= '<div style="background:' . $bgColor . '; padding:4px 2px; text-align:center; min-height:45px; position:relative; border:1px solid ' . $borderColor . ' !important; ' . $clickableStyle . '" ' . $clickHandler . '>'; 26961d05cddcSAtari911 26979ccd446eSAtari911 // Day letter - theme color 26989ccd446eSAtari911 $dayLetterColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary']; 26999ccd446eSAtari911 $html .= '<div style="font-size:9px; color:' . $dayLetterColor . '; font-weight:500; font-family:system-ui, sans-serif;">' . $dayNames[$i] . '</div>'; 27001d05cddcSAtari911 27011d05cddcSAtari911 // Day number 27021d05cddcSAtari911 $html .= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:2px 0; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNum . '</div>'; 27031d05cddcSAtari911 27049ccd446eSAtari911 // Event bars (max 4 visible) with theme-aware glow 27051d05cddcSAtari911 if ($eventCount > 0) { 27069ccd446eSAtari911 $showCount = min($eventCount, 4); 27071d05cddcSAtari911 for ($j = 0; $j < $showCount; $j++) { 27081d05cddcSAtari911 $event = $events[$j]; 27099ccd446eSAtari911 $color = isset($event['color']) ? $event['color'] : $themeStyles['text_primary']; 27109ccd446eSAtari911 $barShadow = $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . htmlspecialchars($color); 27119ccd446eSAtari911 $html .= '<div style="height:2px; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:' . $barShadow . ';"></div>'; 27121d05cddcSAtari911 } 27131d05cddcSAtari911 27149ccd446eSAtari911 // Show "+N more" if more than 4 - theme color 27159ccd446eSAtari911 if ($eventCount > 4) { 27169ccd446eSAtari911 $moreTextColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary']; 27179ccd446eSAtari911 $html .= '<div style="font-size:7px; color:' . $moreTextColor . '; margin-top:1px; font-family:system-ui, sans-serif;">+' . ($eventCount - 4) . '</div>'; 27181d05cddcSAtari911 } 27191d05cddcSAtari911 } 27201d05cddcSAtari911 27211d05cddcSAtari911 $html .= '</div>'; 27221d05cddcSAtari911 } 27231d05cddcSAtari911 27241d05cddcSAtari911 $html .= '</div>'; 27251d05cddcSAtari911 27269ccd446eSAtari911 // Add container for selected day events display (with unique ID) - theme-aware 27279ccd446eSAtari911 $panelBorderColor = $theme === 'matrix' ? '#00cc07' : 27289ccd446eSAtari911 ($theme === 'purple' ? '#9b59b6' : 27299ccd446eSAtari911 ($theme === 'pink' ? '#ff1493' : 27309ccd446eSAtari911 ($theme === 'wiki' ? $themeStyles['border'] : '#3498db'))); 27319ccd446eSAtari911 $panelHeaderBg = $theme === 'matrix' ? '#00cc07' : 27329ccd446eSAtari911 ($theme === 'purple' ? '#9b59b6' : 27339ccd446eSAtari911 ($theme === 'pink' ? '#ff1493' : 27349ccd446eSAtari911 ($theme === 'wiki' ? $themeStyles['border'] : '#3498db'))); 27359ccd446eSAtari911 $panelShadow = $theme === 'matrix' ? '0 0 5px rgba(0, 204, 7, 0.2)' : 27369ccd446eSAtari911 ($theme === 'purple' ? '0 0 5px rgba(155, 89, 182, 0.2)' : 27379ccd446eSAtari911 ($theme === 'pink' ? '0 0 8px rgba(255, 20, 147, 0.4)' : 27389ccd446eSAtari911 '0 1px 3px rgba(0, 0, 0, 0.1)')); 27399ccd446eSAtari911 $panelContentBg = $theme === 'professional' ? 'rgba(255, 255, 255, 0.95)' : 27409ccd446eSAtari911 ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.5)'); 27419ccd446eSAtari911 $panelHeaderShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $panelHeaderBg; 27429ccd446eSAtari911 27439ccd446eSAtari911 // Header text color - white for colored headers, dark for light headers 27449ccd446eSAtari911 $panelHeaderColor = ($theme === 'wiki' || $theme === 'professional') ? '#fff' : '#000'; 27459ccd446eSAtari911 27469ccd446eSAtari911 $html .= '<div id="selected-day-events-' . $calId . '" style="display:none; margin:8px 4px; border-left:3px solid ' . $panelBorderColor . '; box-shadow:' . $panelShadow . ';">'; 27479ccd446eSAtari911 $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;">'; 27481d05cddcSAtari911 $html .= '<span id="selected-day-title-' . $calId . '"></span>'; 27499ccd446eSAtari911 $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>'; 27501d05cddcSAtari911 $html .= '</div>'; 27519ccd446eSAtari911 $html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:' . $panelContentBg . ';"></div>'; 27521d05cddcSAtari911 $html .= '</div>'; 27531d05cddcSAtari911 27541d05cddcSAtari911 // Add JavaScript for day selection with event data 27551d05cddcSAtari911 $html .= '<script>'; 27561d05cddcSAtari911 // Sanitize calId for JavaScript variable names 27571d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); 27581d05cddcSAtari911 $html .= 'window.weekEventsData_' . $jsCalId . ' = ' . json_encode($weekEvents) . ';'; 27599ccd446eSAtari911 27609ccd446eSAtari911 // Pass theme colors to JavaScript 27619ccd446eSAtari911 $jsThemeColors = json_encode([ 27629ccd446eSAtari911 'text_primary' => $themeStyles['text_primary'], 27639ccd446eSAtari911 'text_bright' => $themeStyles['text_bright'], 27649ccd446eSAtari911 'text_dim' => $themeStyles['text_dim'], 27659ccd446eSAtari911 'text_shadow' => ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $themeStyles['text_primary'] : '', 27669ccd446eSAtari911 'event_bg' => $theme === 'professional' ? 'rgba(255, 255, 255, 0.5)' : 27679ccd446eSAtari911 ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.3)'), 27689ccd446eSAtari911 'border_color' => $theme === 'professional' ? 'rgba(0, 0, 0, 0.1)' : 27699ccd446eSAtari911 ($theme === 'purple' ? 'rgba(155, 89, 182, 0.2)' : 27709ccd446eSAtari911 ($theme === 'pink' ? 'rgba(255, 20, 147, 0.3)' : 27719ccd446eSAtari911 ($theme === 'wiki' ? $themeStyles['grid_border'] : 'rgba(0, 204, 7, 0.2)'))), 27729ccd446eSAtari911 'bar_shadow' => $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' : 27739ccd446eSAtari911 ($theme === 'wiki' ? '0 1px 2px rgba(0,0,0,0.15)' : '0 0 3px') 27749ccd446eSAtari911 ]); 27759ccd446eSAtari911 $html .= 'window.themeColors_' . $jsCalId . ' = ' . $jsThemeColors . ';'; 27761d05cddcSAtari911 $html .= ' 27771d05cddcSAtari911 window.showDayEvents_' . $jsCalId . ' = function(dateKey) { 27781d05cddcSAtari911 const eventsData = window.weekEventsData_' . $jsCalId . '; 27791d05cddcSAtari911 const container = document.getElementById("selected-day-events-' . $calId . '"); 27801d05cddcSAtari911 const title = document.getElementById("selected-day-title-' . $calId . '"); 27811d05cddcSAtari911 const content = document.getElementById("selected-day-content-' . $calId . '"); 27821d05cddcSAtari911 27831d05cddcSAtari911 if (!eventsData[dateKey] || eventsData[dateKey].length === 0) return; 27841d05cddcSAtari911 27851d05cddcSAtari911 // Format date for display 27861d05cddcSAtari911 const dateObj = new Date(dateKey + "T00:00:00"); 27871d05cddcSAtari911 const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" }); 27881d05cddcSAtari911 const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" }); 27891d05cddcSAtari911 title.textContent = dayName + ", " + monthDay; 27901d05cddcSAtari911 27911d05cddcSAtari911 // Clear content 27921d05cddcSAtari911 content.innerHTML = ""; 27931d05cddcSAtari911 2794231d0edbSAtari911 // Sort events by time (all-day events first, then timed events chronologically) 27951d05cddcSAtari911 const sortedEvents = [...eventsData[dateKey]].sort((a, b) => { 2796231d0edbSAtari911 // All-day events (no time) go to the beginning 27971d05cddcSAtari911 if (!a.time && !b.time) return 0; 2798231d0edbSAtari911 if (!a.time) return -1; // a is all-day, comes first 2799231d0edbSAtari911 if (!b.time) return 1; // b is all-day, comes first 28001d05cddcSAtari911 28011d05cddcSAtari911 // Compare times (format: "HH:MM") 28021d05cddcSAtari911 const timeA = a.time.split(":").map(Number); 28031d05cddcSAtari911 const timeB = b.time.split(":").map(Number); 28041d05cddcSAtari911 const minutesA = timeA[0] * 60 + timeA[1]; 28051d05cddcSAtari911 const minutesB = timeB[0] * 60 + timeB[1]; 28061d05cddcSAtari911 28071d05cddcSAtari911 return minutesA - minutesB; 28081d05cddcSAtari911 }); 28091d05cddcSAtari911 28109ccd446eSAtari911 // Build events HTML with single color bar (event color only) - theme-aware 28119ccd446eSAtari911 const themeColors = window.themeColors_' . $jsCalId . '; 28121d05cddcSAtari911 sortedEvents.forEach(event => { 28139ccd446eSAtari911 const eventColor = event.color || themeColors.text_primary; 28141d05cddcSAtari911 28151d05cddcSAtari911 const eventDiv = document.createElement("div"); 28169ccd446eSAtari911 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;"; 28171d05cddcSAtari911 28181d05cddcSAtari911 let eventHTML = ""; 28191d05cddcSAtari911 28209ccd446eSAtari911 // Event assigned color bar (single bar on left) - theme-aware shadow 28219ccd446eSAtari911 const barShadow = themeColors.bar_shadow + (themeColors.bar_shadow.includes("rgba") ? "" : " " + eventColor); 28229ccd446eSAtari911 eventHTML += "<div style=\\"width:3px; align-self:stretch; background:" + eventColor + "; border-radius:1px; flex-shrink:0; box-shadow:" + barShadow + ";\\"></div>"; 28231d05cddcSAtari911 2824231d0edbSAtari911 // Content wrapper 2825231d0edbSAtari911 eventHTML += "<div style=\\"flex:1; min-width:0; display:flex; justify-content:space-between; align-items:start; gap:4px;\\">"; 28261d05cddcSAtari911 2827231d0edbSAtari911 // Left side: event details 28281d05cddcSAtari911 eventHTML += "<div style=\\"flex:1; min-width:0;\\">"; 28299ccd446eSAtari911 eventHTML += "<div style=\\"font-weight:600; color:" + themeColors.text_primary + "; word-wrap:break-word; font-family:system-ui, sans-serif; " + themeColors.text_shadow + ";\\">"; 28301d05cddcSAtari911 28311d05cddcSAtari911 // Time 28321d05cddcSAtari911 if (event.time) { 28331d05cddcSAtari911 const timeParts = event.time.split(":"); 28341d05cddcSAtari911 let hours = parseInt(timeParts[0]); 28351d05cddcSAtari911 const minutes = timeParts[1]; 28361d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 28371d05cddcSAtari911 hours = hours % 12 || 12; 28389ccd446eSAtari911 eventHTML += "<span style=\\"color:" + themeColors.text_bright + "; font-weight:500; font-size:9px;\\">" + hours + ":" + minutes + " " + ampm + "</span> "; 28391d05cddcSAtari911 } 28401d05cddcSAtari911 28411d05cddcSAtari911 // Title - use HTML version if available 28421d05cddcSAtari911 const titleHTML = event.title_html || event.title || "Untitled"; 28431d05cddcSAtari911 eventHTML += titleHTML; 28441d05cddcSAtari911 eventHTML += "</div>"; 28451d05cddcSAtari911 28469ccd446eSAtari911 // Description if present - use HTML version - theme-aware color 28471d05cddcSAtari911 if (event.description_html || event.description) { 28481d05cddcSAtari911 const descHTML = event.description_html || event.description; 28499ccd446eSAtari911 eventHTML += "<div style=\\"font-size:9px; color:" + themeColors.text_dim + "; margin-top:2px;\\">" + descHTML + "</div>"; 28501d05cddcSAtari911 } 28511d05cddcSAtari911 2852231d0edbSAtari911 eventHTML += "</div>"; // Close event details 2853231d0edbSAtari911 28549ccd446eSAtari911 // Right side: conflict badge with tooltip 2855231d0edbSAtari911 if (event.conflict) { 28569ccd446eSAtari911 let conflictList = []; 28579ccd446eSAtari911 if (event.conflictingWith && event.conflictingWith.length > 0) { 28589ccd446eSAtari911 event.conflictingWith.forEach(conf => { 28599ccd446eSAtari911 const confTime = conf.time + (conf.end_time ? " - " + conf.end_time : ""); 28609ccd446eSAtari911 conflictList.push(conf.title + " (" + confTime + ")"); 28619ccd446eSAtari911 }); 28629ccd446eSAtari911 } 28639ccd446eSAtari911 const conflictData = btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))); 28649ccd446eSAtari911 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>"; 2865231d0edbSAtari911 } 2866231d0edbSAtari911 2867231d0edbSAtari911 eventHTML += "</div>"; // Close content wrapper 28681d05cddcSAtari911 28691d05cddcSAtari911 eventDiv.innerHTML = eventHTML; 28701d05cddcSAtari911 content.appendChild(eventDiv); 28711d05cddcSAtari911 }); 28721d05cddcSAtari911 28731d05cddcSAtari911 container.style.display = "block"; 28741d05cddcSAtari911 }; 28751d05cddcSAtari911 '; 28761d05cddcSAtari911 $html .= '</script>'; 28771d05cddcSAtari911 28781d05cddcSAtari911 return $html; 28791d05cddcSAtari911 } 28801d05cddcSAtari911 28811d05cddcSAtari911 /** 28821d05cddcSAtari911 * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders 28831d05cddcSAtari911 */ 28849ccd446eSAtari911 private function renderSidebarSection($title, $events, $accentColor, $calId, $themeStyles, $theme) { 28851d05cddcSAtari911 // Keep the original accent colors for borders 28861d05cddcSAtari911 $borderColor = $accentColor; 28871d05cddcSAtari911 28881d05cddcSAtari911 // Show date for Important Events section 28891d05cddcSAtari911 $showDate = ($title === 'Important Events'); 28901d05cddcSAtari911 28919ccd446eSAtari911 // Sort events differently based on section 28929ccd446eSAtari911 if ($title === 'Important Events') { 28939ccd446eSAtari911 // Important Events: sort by date first, then by time 28949ccd446eSAtari911 usort($events, function($a, $b) { 28959ccd446eSAtari911 $aDate = isset($a['date']) ? $a['date'] : ''; 28969ccd446eSAtari911 $bDate = isset($b['date']) ? $b['date'] : ''; 28971d05cddcSAtari911 28989ccd446eSAtari911 // Different dates - sort by date 28999ccd446eSAtari911 if ($aDate !== $bDate) { 29009ccd446eSAtari911 return strcmp($aDate, $bDate); 29019ccd446eSAtari911 } 29029ccd446eSAtari911 29039ccd446eSAtari911 // Same date - sort by time 29049ccd446eSAtari911 $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : ''; 29059ccd446eSAtari911 $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : ''; 29069ccd446eSAtari911 29079ccd446eSAtari911 // All-day events last within same date 29089ccd446eSAtari911 if (empty($aTime) && !empty($bTime)) return 1; 29099ccd446eSAtari911 if (!empty($aTime) && empty($bTime)) return -1; 29109ccd446eSAtari911 if (empty($aTime) && empty($bTime)) return 0; 29119ccd446eSAtari911 29129ccd446eSAtari911 // Both have times 29139ccd446eSAtari911 $aMinutes = $this->timeToMinutes($aTime); 29149ccd446eSAtari911 $bMinutes = $this->timeToMinutes($bTime); 29159ccd446eSAtari911 return $aMinutes - $bMinutes; 29169ccd446eSAtari911 }); 29179ccd446eSAtari911 } else { 29189ccd446eSAtari911 // Today/Tomorrow: sort by time only (all same date) 29199ccd446eSAtari911 usort($events, function($a, $b) { 29209ccd446eSAtari911 $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : ''; 29219ccd446eSAtari911 $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : ''; 29229ccd446eSAtari911 29239ccd446eSAtari911 // All-day events (no time) come first 29249ccd446eSAtari911 if (empty($aTime) && !empty($bTime)) return -1; 29259ccd446eSAtari911 if (!empty($aTime) && empty($bTime)) return 1; 29269ccd446eSAtari911 if (empty($aTime) && empty($bTime)) return 0; 29279ccd446eSAtari911 29289ccd446eSAtari911 // Both have times - convert to minutes for proper chronological sort 29299ccd446eSAtari911 $aMinutes = $this->timeToMinutes($aTime); 29309ccd446eSAtari911 $bMinutes = $this->timeToMinutes($bTime); 29319ccd446eSAtari911 29329ccd446eSAtari911 return $aMinutes - $bMinutes; 29339ccd446eSAtari911 }); 29349ccd446eSAtari911 } 29359ccd446eSAtari911 29369ccd446eSAtari911 // Theme-aware section shadow 29379ccd446eSAtari911 $sectionShadow = $theme === 'matrix' ? '0 0 5px rgba(0, 204, 7, 0.2)' : 29389ccd446eSAtari911 ($theme === 'purple' ? '0 0 5px rgba(155, 89, 182, 0.2)' : 29399ccd446eSAtari911 ($theme === 'pink' ? '0 0 8px rgba(255, 20, 147, 0.4)' : 29409ccd446eSAtari911 '0 1px 3px rgba(0, 0, 0, 0.1)')); 29419ccd446eSAtari911 29429ccd446eSAtari911 $html = '<div style="border-left:3px solid ' . $borderColor . '; margin:8px 4px; box-shadow:' . $sectionShadow . ';">'; 29439ccd446eSAtari911 29449ccd446eSAtari911 // Section header with accent color background - theme-aware shadow 29459ccd446eSAtari911 $headerShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $accentColor; 29469ccd446eSAtari911 $headerTextColor = ($theme === 'wiki') ? '#fff' : '#000'; 29479ccd446eSAtari911 $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 . ';">'; 29481d05cddcSAtari911 $html .= htmlspecialchars($title); 29491d05cddcSAtari911 $html .= '</div>'; 29501d05cddcSAtari911 29519ccd446eSAtari911 // Events - no background (transparent) 29529ccd446eSAtari911 $html .= '<div style="padding:4px 0;">'; 29531d05cddcSAtari911 29541d05cddcSAtari911 foreach ($events as $event) { 29559ccd446eSAtari911 $html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor, $themeStyles, $theme); 29561d05cddcSAtari911 } 29571d05cddcSAtari911 29581d05cddcSAtari911 $html .= '</div>'; 29591d05cddcSAtari911 $html .= '</div>'; 29601d05cddcSAtari911 29611d05cddcSAtari911 return $html; 29621d05cddcSAtari911 } 29631d05cddcSAtari911 29641d05cddcSAtari911 /** 29659ccd446eSAtari911 * Render individual event in sidebar - Theme-aware 29661d05cddcSAtari911 */ 29679ccd446eSAtari911 private function renderSidebarEvent($event, $calId, $showDate = false, $sectionColor = '#00cc07', $themeStyles = null, $theme = 'matrix') { 29681d05cddcSAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 29691d05cddcSAtari911 $time = isset($event['time']) ? $event['time'] : ''; 29701d05cddcSAtari911 $endTime = isset($event['endTime']) ? $event['endTime'] : ''; 29719ccd446eSAtari911 $eventColor = isset($event['color']) ? htmlspecialchars($event['color']) : ($themeStyles ? $themeStyles['text_primary'] : '#00cc07'); 29721d05cddcSAtari911 $date = isset($event['date']) ? $event['date'] : ''; 29731d05cddcSAtari911 $isTask = isset($event['isTask']) && $event['isTask']; 29741d05cddcSAtari911 $completed = isset($event['completed']) && $event['completed']; 29751d05cddcSAtari911 29769ccd446eSAtari911 // Theme-aware colors 29779ccd446eSAtari911 $titleColor = $themeStyles ? $themeStyles['text_primary'] : '#00cc07'; 29789ccd446eSAtari911 $timeColor = $themeStyles ? $themeStyles['text_bright'] : '#00dd00'; 29799ccd446eSAtari911 $textShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $titleColor . ';' : ''; 29801d05cddcSAtari911 29819ccd446eSAtari911 // Check for conflicts (using 'conflict' field set by detectTimeConflicts) 29829ccd446eSAtari911 $hasConflict = isset($event['conflict']) && $event['conflict']; 29839ccd446eSAtari911 $conflictingWith = isset($event['conflictingWith']) ? $event['conflictingWith'] : []; 29849ccd446eSAtari911 29859ccd446eSAtari911 // Build conflict list for tooltip 29869ccd446eSAtari911 $conflictList = []; 29879ccd446eSAtari911 if ($hasConflict && !empty($conflictingWith)) { 29889ccd446eSAtari911 foreach ($conflictingWith as $conf) { 29899ccd446eSAtari911 $confTime = $this->formatTimeDisplay($conf['time'], isset($conf['end_time']) ? $conf['end_time'] : ''); 29909ccd446eSAtari911 $conflictList[] = $conf['title'] . ' (' . $confTime . ')'; 29919ccd446eSAtari911 } 29929ccd446eSAtari911 } 29939ccd446eSAtari911 29949ccd446eSAtari911 // No background on individual events (transparent) 29959ccd446eSAtari911 // Use theme grid_border with slight opacity for subtle divider 29969ccd446eSAtari911 $borderColor = $themeStyles['grid_border']; 29979ccd446eSAtari911 29989ccd446eSAtari911 $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;">'; 29991d05cddcSAtari911 3000231d0edbSAtari911 // Event's assigned color bar (single bar on the left) 30019ccd446eSAtari911 $barShadow = ($theme === 'professional') ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . $eventColor; 30029ccd446eSAtari911 $html .= '<div style="width:3px; align-self:stretch; background:' . $eventColor . '; border-radius:1px; flex-shrink:0; box-shadow:' . $barShadow . ';"></div>'; 30031d05cddcSAtari911 30041d05cddcSAtari911 // Content 30051d05cddcSAtari911 $html .= '<div style="flex:1; min-width:0;">'; 30061d05cddcSAtari911 30071d05cddcSAtari911 // Time + title 30089ccd446eSAtari911 $html .= '<div style="font-weight:600; color:' . $titleColor . '; word-wrap:break-word; font-family:system-ui, sans-serif; ' . $textShadow . '">'; 30091d05cddcSAtari911 30101d05cddcSAtari911 if ($time) { 30111d05cddcSAtari911 $displayTime = $this->formatTimeDisplay($time, $endTime); 30129ccd446eSAtari911 $html .= '<span style="color:' . $timeColor . '; font-weight:500; font-size:9px;">' . htmlspecialchars($displayTime) . '</span> '; 30131d05cddcSAtari911 } 30141d05cddcSAtari911 30151d05cddcSAtari911 // Task checkbox 30161d05cddcSAtari911 if ($isTask) { 30171d05cddcSAtari911 $checkIcon = $completed ? '☑' : '☐'; 30189ccd446eSAtari911 $checkColor = $themeStyles ? $themeStyles['text_bright'] : '#00ff00'; 30199ccd446eSAtari911 $html .= '<span style="font-size:11px; color:' . $checkColor . ';">' . $checkIcon . '</span> '; 30201d05cddcSAtari911 } 30211d05cddcSAtari911 30229ccd446eSAtari911 $html .= $title; // Already HTML-escaped on line 2625 30231d05cddcSAtari911 30249ccd446eSAtari911 // Conflict badge using same system as main calendar 30259ccd446eSAtari911 if ($hasConflict && !empty($conflictList)) { 30269ccd446eSAtari911 $conflictJson = base64_encode(json_encode($conflictList)); 30279ccd446eSAtari911 $html .= ' <span class="event-conflict-badge" style="font-size:10px;" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . count($conflictList) . '</span>'; 30281d05cddcSAtari911 } 30291d05cddcSAtari911 30301d05cddcSAtari911 $html .= '</div>'; 30311d05cddcSAtari911 30321d05cddcSAtari911 // Date display BELOW event name for Important events 30331d05cddcSAtari911 if ($showDate && $date) { 30341d05cddcSAtari911 $dateObj = new DateTime($date); 30351d05cddcSAtari911 $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10" 30369ccd446eSAtari911 $dateColor = $themeStyles ? $themeStyles['text_dim'] : '#00aa00'; 30379ccd446eSAtari911 $dateShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $dateColor . ';' : ''; 30389ccd446eSAtari911 $html .= '<div style="font-size:8px; color:' . $dateColor . '; font-weight:500; margin-top:2px; ' . $dateShadow . '">' . htmlspecialchars($displayDate) . '</div>'; 30391d05cddcSAtari911 } 30401d05cddcSAtari911 30411d05cddcSAtari911 $html .= '</div>'; 30421d05cddcSAtari911 $html .= '</div>'; 30431d05cddcSAtari911 30441d05cddcSAtari911 return $html; 30451d05cddcSAtari911 } 30461d05cddcSAtari911 30471d05cddcSAtari911 /** 30481d05cddcSAtari911 * Format time display (12-hour format with optional end time) 30491d05cddcSAtari911 */ 30501d05cddcSAtari911 private function formatTimeDisplay($startTime, $endTime = '') { 30511d05cddcSAtari911 // Convert start time 30521d05cddcSAtari911 list($hour, $minute) = explode(':', $startTime); 30531d05cddcSAtari911 $hour = (int)$hour; 30541d05cddcSAtari911 $ampm = $hour >= 12 ? 'PM' : 'AM'; 30551d05cddcSAtari911 $displayHour = $hour % 12; 30561d05cddcSAtari911 if ($displayHour === 0) $displayHour = 12; 30571d05cddcSAtari911 30581d05cddcSAtari911 $display = $displayHour . ':' . $minute . ' ' . $ampm; 30591d05cddcSAtari911 30601d05cddcSAtari911 // Add end time if provided 30611d05cddcSAtari911 if ($endTime && $endTime !== '') { 30621d05cddcSAtari911 list($endHour, $endMinute) = explode(':', $endTime); 30631d05cddcSAtari911 $endHour = (int)$endHour; 30641d05cddcSAtari911 $endAmpm = $endHour >= 12 ? 'PM' : 'AM'; 30651d05cddcSAtari911 $endDisplayHour = $endHour % 12; 30661d05cddcSAtari911 if ($endDisplayHour === 0) $endDisplayHour = 12; 30671d05cddcSAtari911 30681d05cddcSAtari911 $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm; 30691d05cddcSAtari911 } 30701d05cddcSAtari911 30711d05cddcSAtari911 return $display; 30721d05cddcSAtari911 } 30731d05cddcSAtari911 30741d05cddcSAtari911 /** 30759ccd446eSAtari911 * Detect time conflicts among events on the same day 30769ccd446eSAtari911 * Returns events array with 'conflict' flag and 'conflictingWith' array 30779ccd446eSAtari911 */ 30789ccd446eSAtari911 private function detectTimeConflicts($dayEvents) { 30799ccd446eSAtari911 if (empty($dayEvents)) { 30809ccd446eSAtari911 return $dayEvents; 30819ccd446eSAtari911 } 30829ccd446eSAtari911 30839ccd446eSAtari911 // If only 1 event, no conflicts possible but still add the flag 30849ccd446eSAtari911 if (count($dayEvents) === 1) { 30859ccd446eSAtari911 return [array_merge($dayEvents[0], ['conflict' => false, 'conflictingWith' => []])]; 30869ccd446eSAtari911 } 30879ccd446eSAtari911 30889ccd446eSAtari911 $eventsWithFlags = []; 30899ccd446eSAtari911 30909ccd446eSAtari911 foreach ($dayEvents as $i => $event) { 30919ccd446eSAtari911 $hasConflict = false; 30929ccd446eSAtari911 $conflictingWith = []; 30939ccd446eSAtari911 30949ccd446eSAtari911 // Skip all-day events (no time) 30959ccd446eSAtari911 if (empty($event['time'])) { 30969ccd446eSAtari911 $eventsWithFlags[] = array_merge($event, ['conflict' => false, 'conflictingWith' => []]); 30979ccd446eSAtari911 continue; 30989ccd446eSAtari911 } 30999ccd446eSAtari911 31009ccd446eSAtari911 // Get this event's time range 31019ccd446eSAtari911 $startTime = $event['time']; 31029ccd446eSAtari911 // Check both 'end_time' (snake_case) and 'endTime' (camelCase) for compatibility 31039ccd446eSAtari911 $endTime = ''; 31049ccd446eSAtari911 if (isset($event['end_time']) && $event['end_time'] !== '') { 31059ccd446eSAtari911 $endTime = $event['end_time']; 31069ccd446eSAtari911 } elseif (isset($event['endTime']) && $event['endTime'] !== '') { 31079ccd446eSAtari911 $endTime = $event['endTime']; 31089ccd446eSAtari911 } else { 31099ccd446eSAtari911 // If no end time, use start time (zero duration) - matches main calendar logic 31109ccd446eSAtari911 $endTime = $startTime; 31119ccd446eSAtari911 } 31129ccd446eSAtari911 31139ccd446eSAtari911 // Check against all other events 31149ccd446eSAtari911 foreach ($dayEvents as $j => $otherEvent) { 31159ccd446eSAtari911 if ($i === $j) continue; // Skip self 31169ccd446eSAtari911 if (empty($otherEvent['time'])) continue; // Skip all-day events 31179ccd446eSAtari911 31189ccd446eSAtari911 $otherStart = $otherEvent['time']; 31199ccd446eSAtari911 // Check both field name formats 31209ccd446eSAtari911 $otherEnd = ''; 31219ccd446eSAtari911 if (isset($otherEvent['end_time']) && $otherEvent['end_time'] !== '') { 31229ccd446eSAtari911 $otherEnd = $otherEvent['end_time']; 31239ccd446eSAtari911 } elseif (isset($otherEvent['endTime']) && $otherEvent['endTime'] !== '') { 31249ccd446eSAtari911 $otherEnd = $otherEvent['endTime']; 31259ccd446eSAtari911 } else { 31269ccd446eSAtari911 $otherEnd = $otherStart; 31279ccd446eSAtari911 } 31289ccd446eSAtari911 31299ccd446eSAtari911 // Check for overlap: convert to minutes and compare 31309ccd446eSAtari911 $start1Min = $this->timeToMinutes($startTime); 31319ccd446eSAtari911 $end1Min = $this->timeToMinutes($endTime); 31329ccd446eSAtari911 $start2Min = $this->timeToMinutes($otherStart); 31339ccd446eSAtari911 $end2Min = $this->timeToMinutes($otherEnd); 31349ccd446eSAtari911 31359ccd446eSAtari911 // Overlap if: start1 < end2 AND start2 < end1 31369ccd446eSAtari911 // Note: Using < (not <=) so events that just touch at boundaries don't conflict 31379ccd446eSAtari911 // e.g., 1:00-2:00 and 2:00-3:00 are NOT in conflict 31389ccd446eSAtari911 if ($start1Min < $end2Min && $start2Min < $end1Min) { 31399ccd446eSAtari911 $hasConflict = true; 31409ccd446eSAtari911 $conflictingWith[] = [ 31419ccd446eSAtari911 'title' => isset($otherEvent['title']) ? $otherEvent['title'] : 'Untitled', 31429ccd446eSAtari911 'time' => $otherStart, 31439ccd446eSAtari911 'end_time' => $otherEnd 31449ccd446eSAtari911 ]; 31459ccd446eSAtari911 } 31469ccd446eSAtari911 } 31479ccd446eSAtari911 31489ccd446eSAtari911 $eventsWithFlags[] = array_merge($event, [ 31499ccd446eSAtari911 'conflict' => $hasConflict, 31509ccd446eSAtari911 'conflictingWith' => $conflictingWith 31519ccd446eSAtari911 ]); 31529ccd446eSAtari911 } 31539ccd446eSAtari911 31549ccd446eSAtari911 return $eventsWithFlags; 31559ccd446eSAtari911 } 31569ccd446eSAtari911 31579ccd446eSAtari911 /** 31589ccd446eSAtari911 * Add hours to a time string 31599ccd446eSAtari911 */ 31609ccd446eSAtari911 private function addHoursToTime($time, $hours) { 31619ccd446eSAtari911 $totalMinutes = $this->timeToMinutes($time) + ($hours * 60); 31629ccd446eSAtari911 $h = floor($totalMinutes / 60) % 24; 31639ccd446eSAtari911 $m = $totalMinutes % 60; 31649ccd446eSAtari911 return sprintf('%02d:%02d', $h, $m); 31659ccd446eSAtari911 } 31669ccd446eSAtari911 31679ccd446eSAtari911 /** 31681d05cddcSAtari911 * Render DokuWiki syntax to HTML 31691d05cddcSAtari911 * Converts **bold**, //italic//, [[links]], etc. to HTML 31701d05cddcSAtari911 */ 31711d05cddcSAtari911 private function renderDokuWikiToHtml($text) { 31721d05cddcSAtari911 if (empty($text)) return ''; 31731d05cddcSAtari911 31741d05cddcSAtari911 // Use DokuWiki's parser to render the text 31751d05cddcSAtari911 $instructions = p_get_instructions($text); 31761d05cddcSAtari911 31771d05cddcSAtari911 // Render instructions to XHTML 31781d05cddcSAtari911 $xhtml = p_render('xhtml', $instructions, $info); 31791d05cddcSAtari911 31801d05cddcSAtari911 // Remove surrounding <p> tags if present (we're rendering inline) 31811d05cddcSAtari911 $xhtml = preg_replace('/^<p>(.*)<\/p>$/s', '$1', trim($xhtml)); 31821d05cddcSAtari911 31831d05cddcSAtari911 return $xhtml; 31841d05cddcSAtari911 } 31851d05cddcSAtari911 31861d05cddcSAtari911 // Keep old scanForNamespaces for backward compatibility (not used anymore) 31871d05cddcSAtari911 private function scanForNamespaces($dir, $baseNamespace, &$namespaces) { 31881d05cddcSAtari911 if (!is_dir($dir)) return; 31891d05cddcSAtari911 31901d05cddcSAtari911 $items = scandir($dir); 31911d05cddcSAtari911 foreach ($items as $item) { 31921d05cddcSAtari911 if ($item === '.' || $item === '..' || $item === 'calendar') continue; 31931d05cddcSAtari911 31941d05cddcSAtari911 $path = $dir . $item; 31951d05cddcSAtari911 if (is_dir($path)) { 31961d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 31971d05cddcSAtari911 $namespaces[] = $namespace; 31981d05cddcSAtari911 $this->scanForNamespaces($path . '/', $namespace, $namespaces); 31991d05cddcSAtari911 } 32001d05cddcSAtari911 } 32011d05cddcSAtari911 } 32029ccd446eSAtari911 32039ccd446eSAtari911 /** 32049ccd446eSAtari911 * Get current sidebar theme 32059ccd446eSAtari911 */ 32069ccd446eSAtari911 private function getSidebarTheme() { 32079ccd446eSAtari911 $configFile = DOKU_INC . 'data/meta/calendar_theme.txt'; 32089ccd446eSAtari911 if (file_exists($configFile)) { 32099ccd446eSAtari911 $theme = trim(file_get_contents($configFile)); 32109ccd446eSAtari911 if (in_array($theme, ['matrix', 'purple', 'professional', 'pink', 'wiki'])) { 32119ccd446eSAtari911 return $theme; 32129ccd446eSAtari911 } 32139ccd446eSAtari911 } 32149ccd446eSAtari911 return 'matrix'; // Default 32159ccd446eSAtari911 } 32169ccd446eSAtari911 32179ccd446eSAtari911 /** 32189ccd446eSAtari911 * Get colors from DokuWiki template's style.ini file 32199ccd446eSAtari911 */ 32209ccd446eSAtari911 private function getWikiTemplateColors() { 32219ccd446eSAtari911 global $conf; 32229ccd446eSAtari911 32239ccd446eSAtari911 // Get current template name 32249ccd446eSAtari911 $template = $conf['template']; 32259ccd446eSAtari911 32269ccd446eSAtari911 // Try multiple possible locations for style.ini 32279ccd446eSAtari911 $possiblePaths = [ 32289ccd446eSAtari911 DOKU_INC . 'conf/tpl/' . $template . '/style.ini', 32299ccd446eSAtari911 DOKU_INC . 'lib/tpl/' . $template . '/style.ini', 32309ccd446eSAtari911 ]; 32319ccd446eSAtari911 32329ccd446eSAtari911 $styleIni = null; 32339ccd446eSAtari911 foreach ($possiblePaths as $path) { 32349ccd446eSAtari911 if (file_exists($path)) { 32359ccd446eSAtari911 $styleIni = parse_ini_file($path, true); 32369ccd446eSAtari911 break; 32379ccd446eSAtari911 } 32389ccd446eSAtari911 } 32399ccd446eSAtari911 32409ccd446eSAtari911 if (!$styleIni) { 32419ccd446eSAtari911 return null; // Fall back to CSS variables 32429ccd446eSAtari911 } 32439ccd446eSAtari911 32449ccd446eSAtari911 // Extract color replacements 32459ccd446eSAtari911 $replacements = isset($styleIni['replacements']) ? $styleIni['replacements'] : []; 32469ccd446eSAtari911 32479ccd446eSAtari911 // Map style.ini colors to our theme structure 32489ccd446eSAtari911 $bgSite = isset($replacements['__background_site__']) ? $replacements['__background_site__'] : '#f5f5f5'; 32499ccd446eSAtari911 $background = isset($replacements['__background__']) ? $replacements['__background__'] : '#fff'; 32509ccd446eSAtari911 $bgAlt = isset($replacements['__background_alt__']) ? $replacements['__background_alt__'] : '#e8e8e8'; 32519ccd446eSAtari911 $bgNeu = isset($replacements['__background_neu__']) ? $replacements['__background_neu__'] : '#eee'; 32529ccd446eSAtari911 $text = isset($replacements['__text__']) ? $replacements['__text__'] : '#333'; 32539ccd446eSAtari911 $textAlt = isset($replacements['__text_alt__']) ? $replacements['__text_alt__'] : '#999'; 32549ccd446eSAtari911 $textNeu = isset($replacements['__text_neu__']) ? $replacements['__text_neu__'] : '#666'; 32559ccd446eSAtari911 $border = isset($replacements['__border__']) ? $replacements['__border__'] : '#ccc'; 32569ccd446eSAtari911 $link = isset($replacements['__link__']) ? $replacements['__link__'] : '#2b73b7'; 32579ccd446eSAtari911 $existing = isset($replacements['__existing__']) ? $replacements['__existing__'] : $link; 32589ccd446eSAtari911 32599ccd446eSAtari911 // Build theme colors from template colors 32609ccd446eSAtari911 // ============================================ 32619ccd446eSAtari911 // DokuWiki style.ini → Calendar CSS Variable Mapping 32629ccd446eSAtari911 // ============================================ 32639ccd446eSAtari911 // style.ini key → CSS variable → Used for 32649ccd446eSAtari911 // __background_site__ → --background-site → Container, panel backgrounds 32659ccd446eSAtari911 // __background__ → --cell-bg → Cell/input backgrounds (typically white) 32669ccd446eSAtari911 // __background_alt__ → --background-alt → Hover states, header backgrounds 32679ccd446eSAtari911 // → --background-header 32689ccd446eSAtari911 // __background_neu__ → --cell-today-bg → Today cell highlight 32699ccd446eSAtari911 // __text__ → --text-primary → Primary text, labels, titles 32709ccd446eSAtari911 // __text_neu__ → --text-dim → Secondary text, dates, descriptions 32719ccd446eSAtari911 // __text_alt__ → (not mapped) → Available for future use 32729ccd446eSAtari911 // __border__ → --border-color → Grid lines, input borders 32739ccd446eSAtari911 // → --header-border 32749ccd446eSAtari911 // __link__ → --border-main → Accent color: buttons, badges, active elements 32759ccd446eSAtari911 // → --text-bright → Links, accent text 32769ccd446eSAtari911 // __existing__ → (fallback to __link__)→ Available for future use 32779ccd446eSAtari911 // 32789ccd446eSAtari911 // To customize: edit your template's conf/style.ini [replacements] 32799ccd446eSAtari911 return [ 32809ccd446eSAtari911 'bg' => $bgSite, 32819ccd446eSAtari911 'border' => $link, // Accent color from template links 32829ccd446eSAtari911 'shadow' => 'rgba(0, 0, 0, 0.1)', 32839ccd446eSAtari911 'header_bg' => $bgAlt, // Headers use alt background 32849ccd446eSAtari911 'header_border' => $border, 32859ccd446eSAtari911 'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)', 32869ccd446eSAtari911 'text_primary' => $text, 32879ccd446eSAtari911 'text_bright' => $link, 32889ccd446eSAtari911 'text_dim' => $textNeu, 32899ccd446eSAtari911 'grid_bg' => $bgSite, 32909ccd446eSAtari911 'grid_border' => $border, 32919ccd446eSAtari911 'cell_bg' => $background, // Cells use __background__ (white/light) 32929ccd446eSAtari911 'cell_today_bg' => $bgNeu, 32939ccd446eSAtari911 'bar_glow' => '0 1px 2px', 32949ccd446eSAtari911 ]; 32959ccd446eSAtari911 } 32969ccd446eSAtari911 32979ccd446eSAtari911 /** 32989ccd446eSAtari911 * Get theme-specific color styles 32999ccd446eSAtari911 */ 33009ccd446eSAtari911 private function getSidebarThemeStyles($theme) { 33019ccd446eSAtari911 // For wiki theme, try to read colors from template's style.ini 33029ccd446eSAtari911 if ($theme === 'wiki') { 33039ccd446eSAtari911 $wikiColors = $this->getWikiTemplateColors(); 33049ccd446eSAtari911 if (!empty($wikiColors)) { 33059ccd446eSAtari911 return $wikiColors; 33069ccd446eSAtari911 } 33079ccd446eSAtari911 // Fall through to default wiki colors if reading fails 33089ccd446eSAtari911 } 33099ccd446eSAtari911 33109ccd446eSAtari911 $themes = [ 33119ccd446eSAtari911 'matrix' => [ 33129ccd446eSAtari911 'bg' => '#242424', 33139ccd446eSAtari911 'border' => '#00cc07', 33149ccd446eSAtari911 'shadow' => 'rgba(0, 204, 7, 0.3)', 33159ccd446eSAtari911 'header_bg' => 'linear-gradient(180deg, #2a2a2a 0%, #242424 100%)', 33169ccd446eSAtari911 'header_border' => '#00cc07', 33179ccd446eSAtari911 'header_shadow' => '0 2px 8px rgba(0, 204, 7, 0.3)', 33189ccd446eSAtari911 'text_primary' => '#00cc07', 33199ccd446eSAtari911 'text_bright' => '#00ff00', 33209ccd446eSAtari911 'text_dim' => '#00aa00', 33219ccd446eSAtari911 'grid_bg' => '#1a3d1a', 33229ccd446eSAtari911 'grid_border' => '#00cc07', 33239ccd446eSAtari911 'cell_bg' => '#242424', 33249ccd446eSAtari911 'cell_today_bg' => '#2a4d2a', 33259ccd446eSAtari911 'bar_glow' => '0 0 3px', 33269ccd446eSAtari911 ], 33279ccd446eSAtari911 'purple' => [ 33289ccd446eSAtari911 'bg' => '#2a2030', 33299ccd446eSAtari911 'border' => '#9b59b6', 33309ccd446eSAtari911 'shadow' => 'rgba(155, 89, 182, 0.3)', 33319ccd446eSAtari911 'header_bg' => 'linear-gradient(180deg, #2f2438 0%, #2a2030 100%)', 33329ccd446eSAtari911 'header_border' => '#9b59b6', 33339ccd446eSAtari911 'header_shadow' => '0 2px 8px rgba(155, 89, 182, 0.3)', 33349ccd446eSAtari911 'text_primary' => '#b19cd9', 33359ccd446eSAtari911 'text_bright' => '#d4a5ff', 33369ccd446eSAtari911 'text_dim' => '#8e7ab8', 33379ccd446eSAtari911 'grid_bg' => '#3d2b4d', 33389ccd446eSAtari911 'grid_border' => '#9b59b6', 33399ccd446eSAtari911 'cell_bg' => '#2a2030', 33409ccd446eSAtari911 'cell_today_bg' => '#3d2b4d', 33419ccd446eSAtari911 'bar_glow' => '0 0 3px', 33429ccd446eSAtari911 ], 33439ccd446eSAtari911 'professional' => [ 33449ccd446eSAtari911 'bg' => '#f5f7fa', 33459ccd446eSAtari911 'border' => '#4a90e2', 33469ccd446eSAtari911 'shadow' => 'rgba(74, 144, 226, 0.2)', 33479ccd446eSAtari911 'header_bg' => 'linear-gradient(180deg, #ffffff 0%, #f5f7fa 100%)', 33489ccd446eSAtari911 'header_border' => '#4a90e2', 33499ccd446eSAtari911 'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)', 33509ccd446eSAtari911 'text_primary' => '#2c3e50', 33519ccd446eSAtari911 'text_bright' => '#4a90e2', 33529ccd446eSAtari911 'text_dim' => '#7f8c8d', 33539ccd446eSAtari911 'grid_bg' => '#e8ecf1', 33549ccd446eSAtari911 'grid_border' => '#d0d7de', 33559ccd446eSAtari911 'cell_bg' => '#ffffff', 33569ccd446eSAtari911 'cell_today_bg' => '#dce8f7', 33579ccd446eSAtari911 'bar_glow' => '0 1px 2px', 33589ccd446eSAtari911 ], 33599ccd446eSAtari911 'pink' => [ 33609ccd446eSAtari911 'bg' => '#1a0d14', 33619ccd446eSAtari911 'border' => '#ff1493', 33629ccd446eSAtari911 'shadow' => 'rgba(255, 20, 147, 0.4)', 33639ccd446eSAtari911 'header_bg' => 'linear-gradient(180deg, #2d1a24 0%, #1a0d14 100%)', 33649ccd446eSAtari911 'header_border' => '#ff1493', 33659ccd446eSAtari911 'header_shadow' => '0 0 12px rgba(255, 20, 147, 0.6)', 33669ccd446eSAtari911 'text_primary' => '#ff69b4', 33679ccd446eSAtari911 'text_bright' => '#ff1493', 33689ccd446eSAtari911 'text_dim' => '#ff85c1', 33699ccd446eSAtari911 'grid_bg' => '#2d1a24', 33709ccd446eSAtari911 'grid_border' => '#ff1493', 33719ccd446eSAtari911 'cell_bg' => '#1a0d14', 33729ccd446eSAtari911 'cell_today_bg' => '#3d2030', 33739ccd446eSAtari911 'bar_glow' => '0 0 5px', 33749ccd446eSAtari911 ], 33759ccd446eSAtari911 'wiki' => [ 33769ccd446eSAtari911 'bg' => '#f5f5f5', 33779ccd446eSAtari911 'border' => '#2b73b7', // Use link blue as accent (matches template) 33789ccd446eSAtari911 'shadow' => 'rgba(0, 0, 0, 0.1)', 33799ccd446eSAtari911 'header_bg' => '#e8e8e8', 33809ccd446eSAtari911 'header_border' => '#ccc', 33819ccd446eSAtari911 'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)', 33829ccd446eSAtari911 'text_primary' => '#333', 33839ccd446eSAtari911 'text_bright' => '#2b73b7', 33849ccd446eSAtari911 'text_dim' => '#666', 33859ccd446eSAtari911 'grid_bg' => '#f5f5f5', 33869ccd446eSAtari911 'grid_border' => '#ccc', 33879ccd446eSAtari911 'cell_bg' => '#fff', 33889ccd446eSAtari911 'cell_today_bg' => '#eee', 33899ccd446eSAtari911 'bar_glow' => '0 1px 2px', 33909ccd446eSAtari911 ], 33919ccd446eSAtari911 ]; 33929ccd446eSAtari911 33939ccd446eSAtari911 return isset($themes[$theme]) ? $themes[$theme] : $themes['matrix']; 33949ccd446eSAtari911 } 33959ccd446eSAtari911 33969ccd446eSAtari911 /** 33979ccd446eSAtari911 * Get week start day preference 33989ccd446eSAtari911 */ 33999ccd446eSAtari911 private function getWeekStartDay() { 34009ccd446eSAtari911 $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt'; 34019ccd446eSAtari911 if (file_exists($configFile)) { 34029ccd446eSAtari911 $start = trim(file_get_contents($configFile)); 34039ccd446eSAtari911 if (in_array($start, ['monday', 'sunday'])) { 34049ccd446eSAtari911 return $start; 34059ccd446eSAtari911 } 34069ccd446eSAtari911 } 34079ccd446eSAtari911 return 'sunday'; // Default to Sunday (US/Canada standard) 34089ccd446eSAtari911 } 340919378907SAtari911}