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