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