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 142866e827SAtari911 /** 152866e827SAtari911 * Get the meta directory path (farm-safe) 162866e827SAtari911 * Uses $conf['metadir'] instead of hardcoded DOKU_INC . 'data/meta/' 172866e827SAtari911 */ 182866e827SAtari911 private function metaDir() { 192866e827SAtari911 global $conf; 202866e827SAtari911 return rtrim($conf['metadir'], '/') . '/'; 212866e827SAtari911 } 222866e827SAtari911 232866e827SAtari911 /** 242866e827SAtari911 * Check if the current user has read access to a namespace 252866e827SAtari911 * 262866e827SAtari911 * @param string $namespace Namespace to check (empty = root) 272866e827SAtari911 * @return bool True if user has at least AUTH_READ 282866e827SAtari911 */ 292866e827SAtari911 private function checkNamespaceRead($namespace) { 302866e827SAtari911 if (empty($namespace) || $namespace === '*') return true; 312866e827SAtari911 $ns = str_replace(['*', ';'], '', $namespace); 322866e827SAtari911 if (empty($ns)) return true; 332866e827SAtari911 $perm = auth_quickaclcheck($ns . ':*'); 342866e827SAtari911 return ($perm >= AUTH_READ); 352866e827SAtari911 } 362866e827SAtari911 372866e827SAtari911 /** 382866e827SAtari911 * Get sync config file path (farm-safe) 392866e827SAtari911 * Checks per-animal metadir first, falls back to shared plugin dir 402866e827SAtari911 */ 412866e827SAtari911 private function syncConfigPath() { 422866e827SAtari911 $perAnimal = $this->metaDir() . 'calendar/sync_config.php'; 432866e827SAtari911 if (file_exists($perAnimal)) return $perAnimal; 442866e827SAtari911 return DOKU_PLUGIN . 'calendar/sync_config.php'; 452866e827SAtari911 } 462866e827SAtari911 4719378907SAtari911 public function getType() { 4819378907SAtari911 return 'substition'; 4919378907SAtari911 } 5019378907SAtari911 5119378907SAtari911 public function getPType() { 5219378907SAtari911 return 'block'; 5319378907SAtari911 } 5419378907SAtari911 5519378907SAtari911 public function getSort() { 5619378907SAtari911 return 155; 5719378907SAtari911 } 5819378907SAtari911 5919378907SAtari911 public function connectTo($mode) { 6019378907SAtari911 $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 6119378907SAtari911 $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 6219378907SAtari911 $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 6319378907SAtari911 } 6419378907SAtari911 6519378907SAtari911 public function handle($match, $state, $pos, Doku_Handler $handler) { 6619378907SAtari911 $isEventList = (strpos($match, '{{eventlist') === 0); 6719378907SAtari911 $isEventPanel = (strpos($match, '{{eventpanel') === 0); 6819378907SAtari911 6919378907SAtari911 if ($isEventList) { 7019378907SAtari911 $match = substr($match, 12, -2); 7119378907SAtari911 } elseif ($isEventPanel) { 7219378907SAtari911 $match = substr($match, 13, -2); 7319378907SAtari911 } else { 7419378907SAtari911 $match = substr($match, 10, -2); 7519378907SAtari911 } 7619378907SAtari911 7719378907SAtari911 $params = array( 7819378907SAtari911 'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'), 7919378907SAtari911 'year' => date('Y'), 8019378907SAtari911 'month' => date('n'), 8119378907SAtari911 'namespace' => '', 8219378907SAtari911 'daterange' => '', 83e3a9f44cSAtari911 'date' => '', 84da206178SAtari911 'range' => '', 85da206178SAtari911 'static' => false, 86da206178SAtari911 'title' => '', 87da206178SAtari911 'noprint' => false, 88da206178SAtari911 'theme' => '', 89da206178SAtari911 'locked' => false // Will be set true if month/year specified 9019378907SAtari911 ); 9119378907SAtari911 92da206178SAtari911 // Track if user explicitly set month or year 93da206178SAtari911 $userSetMonth = false; 94da206178SAtari911 $userSetYear = false; 95da206178SAtari911 9619378907SAtari911 if (trim($match)) { 97da206178SAtari911 // Parse parameters, handling quoted strings properly 98da206178SAtari911 // Match: key="value with spaces" OR key=value OR standalone_flag 99da206178SAtari911 preg_match_all('/(\w+)=["\']([^"\']+)["\']|(\w+)=(\S+)|(\w+)/', trim($match), $matches, PREG_SET_ORDER); 100da206178SAtari911 101da206178SAtari911 foreach ($matches as $m) { 102da206178SAtari911 if (!empty($m[1]) && isset($m[2])) { 103da206178SAtari911 // key="quoted value" 104da206178SAtari911 $key = $m[1]; 105da206178SAtari911 $value = $m[2]; 106da206178SAtari911 $params[$key] = $value; 107da206178SAtari911 if ($key === 'month') $userSetMonth = true; 108da206178SAtari911 if ($key === 'year') $userSetYear = true; 109da206178SAtari911 } elseif (!empty($m[3]) && isset($m[4])) { 110da206178SAtari911 // key=unquoted_value 111da206178SAtari911 $key = $m[3]; 112da206178SAtari911 $value = $m[4]; 113da206178SAtari911 $params[$key] = $value; 114da206178SAtari911 if ($key === 'month') $userSetMonth = true; 115da206178SAtari911 if ($key === 'year') $userSetYear = true; 116da206178SAtari911 } elseif (!empty($m[5])) { 117da206178SAtari911 // standalone flag 118da206178SAtari911 $params[$m[5]] = true; 11919378907SAtari911 } 12019378907SAtari911 } 12119378907SAtari911 } 12219378907SAtari911 123da206178SAtari911 // If user explicitly set month or year, lock navigation 124da206178SAtari911 if ($userSetMonth || $userSetYear) { 125da206178SAtari911 $params['locked'] = true; 126da206178SAtari911 } 127da206178SAtari911 12819378907SAtari911 return $params; 12919378907SAtari911 } 13019378907SAtari911 13119378907SAtari911 public function render($mode, Doku_Renderer $renderer, $data) { 13219378907SAtari911 if ($mode !== 'xhtml') return false; 13319378907SAtari911 1347e8ea635SAtari911 // Disable caching - theme can change via admin without page edit 1357e8ea635SAtari911 $renderer->nocache(); 1367e8ea635SAtari911 13719378907SAtari911 if ($data['type'] === 'eventlist') { 13819378907SAtari911 $html = $this->renderStandaloneEventList($data); 13919378907SAtari911 } elseif ($data['type'] === 'eventpanel') { 14019378907SAtari911 $html = $this->renderEventPanelOnly($data); 141da206178SAtari911 } elseif ($data['static']) { 142da206178SAtari911 $html = $this->renderStaticCalendar($data); 14319378907SAtari911 } else { 14419378907SAtari911 $html = $this->renderCompactCalendar($data); 14519378907SAtari911 } 14619378907SAtari911 14719378907SAtari911 $renderer->doc .= $html; 14819378907SAtari911 return true; 14919378907SAtari911 } 15019378907SAtari911 15119378907SAtari911 private function renderCompactCalendar($data) { 15219378907SAtari911 $year = (int)$data['year']; 15319378907SAtari911 $month = (int)$data['month']; 15419378907SAtari911 $namespace = $data['namespace']; 1552866e827SAtari911 $exclude = isset($data['exclude']) ? $data['exclude'] : ''; 1562866e827SAtari911 $excludeList = $this->parseExcludeList($exclude); 15719378907SAtari911 1580c3b6e81SAtari911 // Get theme - prefer inline theme= parameter, fall back to admin default 1590c3b6e81SAtari911 $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme(); 1609ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 1619ccd446eSAtari911 $themeClass = 'calendar-theme-' . $theme; 1629ccd446eSAtari911 1639ccd446eSAtari911 // Determine button text color: professional uses white, others use bg color 1649ccd446eSAtari911 $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg']; 1659ccd446eSAtari911 166e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 167e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 168e3a9f44cSAtari911 169e3a9f44cSAtari911 if ($isMultiNamespace) { 1702866e827SAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList); 171e3a9f44cSAtari911 } else { 17219378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 173e3a9f44cSAtari911 } 17419378907SAtari911 $calId = 'cal_' . md5(serialize($data) . microtime()); 17519378907SAtari911 17619378907SAtari911 $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); 17719378907SAtari911 17819378907SAtari911 $prevMonth = $month - 1; 17919378907SAtari911 $prevYear = $year; 18019378907SAtari911 if ($prevMonth < 1) { 18119378907SAtari911 $prevMonth = 12; 18219378907SAtari911 $prevYear--; 18319378907SAtari911 } 18419378907SAtari911 18519378907SAtari911 $nextMonth = $month + 1; 18619378907SAtari911 $nextYear = $year; 18719378907SAtari911 if ($nextMonth > 12) { 18819378907SAtari911 $nextMonth = 1; 18919378907SAtari911 $nextYear++; 19019378907SAtari911 } 19119378907SAtari911 19296df7d3eSAtari911 // Get important namespaces from config for highlighting 1932866e827SAtari911 $configFile = $this->syncConfigPath(); 19496df7d3eSAtari911 $importantNsList = ['important']; // default 19596df7d3eSAtari911 if (file_exists($configFile)) { 19696df7d3eSAtari911 $config = include $configFile; 19796df7d3eSAtari911 if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) { 19896df7d3eSAtari911 $importantNsList = array_map('trim', explode(',', $config['important_namespaces'])); 19996df7d3eSAtari911 } 20096df7d3eSAtari911 } 20196df7d3eSAtari911 2029ccd446eSAtari911 // Container - all styling via CSS variables 2032866e827SAtari911 $html = '<div class="calendar-compact-container ' . $themeClass . '" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '" data-exclude="' . htmlspecialchars($exclude) . '" data-year="' . $year . '" data-month="' . $month . '" data-theme="' . $theme . '" data-theme-styles="' . htmlspecialchars(json_encode($themeStyles)) . '" data-important-namespaces="' . htmlspecialchars(json_encode($importantNsList)) . '">'; 2049ccd446eSAtari911 2059ccd446eSAtari911 // Inject CSS variables for this calendar instance - all theming flows from here 2069ccd446eSAtari911 $html .= '<style> 2079ccd446eSAtari911 #' . $calId . ' { 2089ccd446eSAtari911 --background-site: ' . $themeStyles['bg'] . '; 2099ccd446eSAtari911 --background-alt: ' . $themeStyles['cell_bg'] . '; 2109ccd446eSAtari911 --background-header: ' . $themeStyles['header_bg'] . '; 2119ccd446eSAtari911 --text-primary: ' . $themeStyles['text_primary'] . '; 2129ccd446eSAtari911 --text-dim: ' . $themeStyles['text_dim'] . '; 2139ccd446eSAtari911 --text-bright: ' . $themeStyles['text_bright'] . '; 2149ccd446eSAtari911 --border-color: ' . $themeStyles['grid_border'] . '; 2159ccd446eSAtari911 --border-main: ' . $themeStyles['border'] . '; 2169ccd446eSAtari911 --cell-bg: ' . $themeStyles['cell_bg'] . '; 2179ccd446eSAtari911 --cell-today-bg: ' . $themeStyles['cell_today_bg'] . '; 2189ccd446eSAtari911 --shadow-color: ' . $themeStyles['shadow'] . '; 2199ccd446eSAtari911 --header-border: ' . $themeStyles['header_border'] . '; 2209ccd446eSAtari911 --header-shadow: ' . $themeStyles['header_shadow'] . '; 2219ccd446eSAtari911 --grid-bg: ' . $themeStyles['grid_bg'] . '; 2229ccd446eSAtari911 --btn-text: ' . $btnTextColor . '; 2237e8ea635SAtari911 --pastdue-color: ' . $themeStyles['pastdue_color'] . '; 2247e8ea635SAtari911 --pastdue-bg: ' . $themeStyles['pastdue_bg'] . '; 2257e8ea635SAtari911 --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . '; 2267e8ea635SAtari911 --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . '; 2277e8ea635SAtari911 --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . '; 2287e8ea635SAtari911 --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . '; 2297e8ea635SAtari911 --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . '; 2309ccd446eSAtari911 } 2319ccd446eSAtari911 #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; } 2329ccd446eSAtari911 #event-search-' . $calId . '::-webkit-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; } 2339ccd446eSAtari911 #event-search-' . $calId . '::-moz-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; } 2349ccd446eSAtari911 #event-search-' . $calId . ':-ms-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; } 2359ccd446eSAtari911 </style>'; 2361d05cddcSAtari911 2371d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 2381d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 2391d05cddcSAtari911 2401d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 2411d05cddcSAtari911 $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>'; 24219378907SAtari911 24319378907SAtari911 // Embed events data as JSON for JavaScript access 24419378907SAtari911 $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>'; 24519378907SAtari911 24619378907SAtari911 // Left side: Calendar 24719378907SAtari911 $html .= '<div class="calendar-compact-left">'; 24819378907SAtari911 24919378907SAtari911 // Header with navigation 25019378907SAtari911 $html .= '<div class="calendar-compact-header">'; 25119378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 252da206178SAtari911 $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . '</h3>'; 25319378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 254da206178SAtari911 $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>'; 25519378907SAtari911 $html .= '</div>'; 25619378907SAtari911 2570c3b6e81SAtari911 // Calendar grid - day name headers as a separate div (avoids Firefox th height issues) 2580c3b6e81SAtari911 $html .= '<div class="calendar-day-headers">'; 2590c3b6e81SAtari911 $html .= '<span>S</span><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span>'; 2600c3b6e81SAtari911 $html .= '</div>'; 26119378907SAtari911 $html .= '<table class="calendar-compact-grid">'; 2620c3b6e81SAtari911 $html .= '<tbody>'; 26319378907SAtari911 26419378907SAtari911 $firstDay = mktime(0, 0, 0, $month, 1, $year); 26519378907SAtari911 $daysInMonth = date('t', $firstDay); 26619378907SAtari911 $dayOfWeek = date('w', $firstDay); 26719378907SAtari911 268e3a9f44cSAtari911 // Build a map of all events with their date ranges for the calendar grid 26987ac9bf3SAtari911 $eventRanges = array(); 270e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 27187ac9bf3SAtari911 foreach ($dayEvents as $evt) { 27287ac9bf3SAtari911 $eventId = isset($evt['id']) ? $evt['id'] : ''; 27387ac9bf3SAtari911 $startDate = $dateKey; 27487ac9bf3SAtari911 $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey; 27587ac9bf3SAtari911 27687ac9bf3SAtari911 // Only process events that touch this month 27787ac9bf3SAtari911 $eventStart = new DateTime($startDate); 27887ac9bf3SAtari911 $eventEnd = new DateTime($endDate); 27987ac9bf3SAtari911 $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month)); 28087ac9bf3SAtari911 $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth)); 28187ac9bf3SAtari911 28287ac9bf3SAtari911 // Skip if event doesn't overlap with current month 28387ac9bf3SAtari911 if ($eventEnd < $monthStart || $eventStart > $monthEnd) { 28487ac9bf3SAtari911 continue; 28587ac9bf3SAtari911 } 28687ac9bf3SAtari911 28787ac9bf3SAtari911 // Create entry for each day the event spans 28887ac9bf3SAtari911 $current = clone $eventStart; 28987ac9bf3SAtari911 while ($current <= $eventEnd) { 29087ac9bf3SAtari911 $currentKey = $current->format('Y-m-d'); 29187ac9bf3SAtari911 29287ac9bf3SAtari911 // Check if this date is in current month 29387ac9bf3SAtari911 $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey); 29487ac9bf3SAtari911 if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) { 29587ac9bf3SAtari911 if (!isset($eventRanges[$currentKey])) { 29687ac9bf3SAtari911 $eventRanges[$currentKey] = array(); 29787ac9bf3SAtari911 } 29887ac9bf3SAtari911 29987ac9bf3SAtari911 // Add event with span information 30087ac9bf3SAtari911 $evt['_span_start'] = $startDate; 30187ac9bf3SAtari911 $evt['_span_end'] = $endDate; 30287ac9bf3SAtari911 $evt['_is_first_day'] = ($currentKey === $startDate); 30387ac9bf3SAtari911 $evt['_is_last_day'] = ($currentKey === $endDate); 30487ac9bf3SAtari911 $evt['_original_date'] = $dateKey; // Keep track of original date 30587ac9bf3SAtari911 30687ac9bf3SAtari911 // Check if event continues from previous month or to next month 30787ac9bf3SAtari911 $evt['_continues_from_prev'] = ($eventStart < $monthStart); 30887ac9bf3SAtari911 $evt['_continues_to_next'] = ($eventEnd > $monthEnd); 30987ac9bf3SAtari911 31087ac9bf3SAtari911 $eventRanges[$currentKey][] = $evt; 31187ac9bf3SAtari911 } 31287ac9bf3SAtari911 31387ac9bf3SAtari911 $current->modify('+1 day'); 31487ac9bf3SAtari911 } 31587ac9bf3SAtari911 } 31687ac9bf3SAtari911 } 31787ac9bf3SAtari911 318*8e9c470bSAtari911 // Assign stable row slots to multi-day events (same algorithm as JS rebuildCalendar) 319*8e9c470bSAtari911 $slotAssignments = array(); // dateKey -> array of {id, slot} 320*8e9c470bSAtari911 $eventSlots = array(); // eventId -> slot number 321*8e9c470bSAtari911 322*8e9c470bSAtari911 $allDates = array_keys($eventRanges); 323*8e9c470bSAtari911 sort($allDates); 324*8e9c470bSAtari911 325*8e9c470bSAtari911 // First pass: multi-day events get stable slots across all days they span 326*8e9c470bSAtari911 foreach ($allDates as $dk) { 327*8e9c470bSAtari911 foreach ($eventRanges[$dk] as $evt) { 328*8e9c470bSAtari911 $eid = isset($evt['id']) ? $evt['id'] : $evt['title']; 329*8e9c470bSAtari911 $isMultiDay = (isset($evt['_span_start']) && isset($evt['_span_end']) && $evt['_span_start'] !== $evt['_span_end']); 330*8e9c470bSAtari911 331*8e9c470bSAtari911 if ($isMultiDay && !isset($eventSlots[$eid])) { 332*8e9c470bSAtari911 // Find lowest slot free across all days this event spans 333*8e9c470bSAtari911 $slot = 0; 334*8e9c470bSAtari911 $found = false; 335*8e9c470bSAtari911 while (!$found) { 336*8e9c470bSAtari911 $found = true; 337*8e9c470bSAtari911 $checkCurrent = new DateTime($evt['_span_start']); 338*8e9c470bSAtari911 $checkEnd = new DateTime($evt['_span_end']); 339*8e9c470bSAtari911 while ($checkCurrent <= $checkEnd) { 340*8e9c470bSAtari911 $checkKey = $checkCurrent->format('Y-m-d'); 341*8e9c470bSAtari911 if (isset($slotAssignments[$checkKey])) { 342*8e9c470bSAtari911 foreach ($slotAssignments[$checkKey] as $assigned) { 343*8e9c470bSAtari911 if ($assigned['slot'] === $slot) { 344*8e9c470bSAtari911 $found = false; 345*8e9c470bSAtari911 break; 346*8e9c470bSAtari911 } 347*8e9c470bSAtari911 } 348*8e9c470bSAtari911 } 349*8e9c470bSAtari911 if (!$found) break; 350*8e9c470bSAtari911 $checkCurrent->modify('+1 day'); 351*8e9c470bSAtari911 } 352*8e9c470bSAtari911 if (!$found) $slot++; 353*8e9c470bSAtari911 } 354*8e9c470bSAtari911 $eventSlots[$eid] = $slot; 355*8e9c470bSAtari911 356*8e9c470bSAtari911 // Reserve on all days 357*8e9c470bSAtari911 $resCurrent = new DateTime($evt['_span_start']); 358*8e9c470bSAtari911 $resEnd = new DateTime($evt['_span_end']); 359*8e9c470bSAtari911 while ($resCurrent <= $resEnd) { 360*8e9c470bSAtari911 $resKey = $resCurrent->format('Y-m-d'); 361*8e9c470bSAtari911 if (!isset($slotAssignments[$resKey])) $slotAssignments[$resKey] = array(); 362*8e9c470bSAtari911 $slotAssignments[$resKey][] = array('id' => $eid, 'slot' => $slot); 363*8e9c470bSAtari911 $resCurrent->modify('+1 day'); 364*8e9c470bSAtari911 } 365*8e9c470bSAtari911 } 366*8e9c470bSAtari911 } 367*8e9c470bSAtari911 } 368*8e9c470bSAtari911 369*8e9c470bSAtari911 // Second pass: single-day events fill remaining slots 370*8e9c470bSAtari911 foreach ($allDates as $dk) { 371*8e9c470bSAtari911 $singleDay = array(); 372*8e9c470bSAtari911 foreach ($eventRanges[$dk] as $evt) { 373*8e9c470bSAtari911 $eid = isset($evt['id']) ? $evt['id'] : $evt['title']; 374*8e9c470bSAtari911 if (!isset($eventSlots[$eid])) { 375*8e9c470bSAtari911 $singleDay[] = $evt; 376*8e9c470bSAtari911 } 377*8e9c470bSAtari911 } 378*8e9c470bSAtari911 // Sort single-day by time 379*8e9c470bSAtari911 usort($singleDay, function($a, $b) { 380*8e9c470bSAtari911 $timeA = isset($a['time']) ? $a['time'] : ''; 381*8e9c470bSAtari911 $timeB = isset($b['time']) ? $b['time'] : ''; 382*8e9c470bSAtari911 if (empty($timeA) && !empty($timeB)) return -1; 383*8e9c470bSAtari911 if (!empty($timeA) && empty($timeB)) return 1; 384*8e9c470bSAtari911 return strcmp($timeA, $timeB); 385*8e9c470bSAtari911 }); 386*8e9c470bSAtari911 foreach ($singleDay as $evt) { 387*8e9c470bSAtari911 $eid = isset($evt['id']) ? $evt['id'] : $evt['title']; 388*8e9c470bSAtari911 $slot = 0; 389*8e9c470bSAtari911 while (true) { 390*8e9c470bSAtari911 $taken = false; 391*8e9c470bSAtari911 if (isset($slotAssignments[$dk])) { 392*8e9c470bSAtari911 foreach ($slotAssignments[$dk] as $a) { 393*8e9c470bSAtari911 if ($a['slot'] === $slot) { $taken = true; break; } 394*8e9c470bSAtari911 } 395*8e9c470bSAtari911 } 396*8e9c470bSAtari911 if (!$taken) break; 397*8e9c470bSAtari911 $slot++; 398*8e9c470bSAtari911 } 399*8e9c470bSAtari911 $eventSlots[$eid] = $slot; 400*8e9c470bSAtari911 if (!isset($slotAssignments[$dk])) $slotAssignments[$dk] = array(); 401*8e9c470bSAtari911 $slotAssignments[$dk][] = array('id' => $eid, 'slot' => $slot); 402*8e9c470bSAtari911 } 403*8e9c470bSAtari911 } 404*8e9c470bSAtari911 40519378907SAtari911 $currentDay = 1; 40619378907SAtari911 $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7); 40719378907SAtari911 40819378907SAtari911 for ($row = 0; $row < $rowCount; $row++) { 40919378907SAtari911 $html .= '<tr>'; 41019378907SAtari911 for ($col = 0; $col < 7; $col++) { 41119378907SAtari911 if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) { 41219378907SAtari911 $html .= '<td class="cal-empty"></td>'; 41319378907SAtari911 } else { 41419378907SAtari911 $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay); 41519378907SAtari911 $isToday = ($dateKey === date('Y-m-d')); 41687ac9bf3SAtari911 $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]); 41719378907SAtari911 41819378907SAtari911 $classes = 'cal-day'; 41919378907SAtari911 if ($isToday) $classes .= ' cal-today'; 42019378907SAtari911 if ($hasEvents) $classes .= ' cal-has-events'; 42119378907SAtari911 422815440faSAtari911 $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" tabindex="0" role="gridcell" aria-label="' . date('F j, Y', strtotime($dateKey)) . ($hasEvents ? ', has events' : '') . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">'; 4239ccd446eSAtari911 4249ccd446eSAtari911 $dayNumClass = $isToday ? 'day-num day-num-today' : 'day-num'; 4259ccd446eSAtari911 $html .= '<span class="' . $dayNumClass . '">' . $currentDay . '</span>'; 42619378907SAtari911 42719378907SAtari911 if ($hasEvents) { 428*8e9c470bSAtari911 // Build slot map for this day 429*8e9c470bSAtari911 $daySlotList = isset($slotAssignments[$dateKey]) ? $slotAssignments[$dateKey] : array(); 430*8e9c470bSAtari911 $maxSlot = -1; 431*8e9c470bSAtari911 foreach ($daySlotList as $ds) { 432*8e9c470bSAtari911 if ($ds['slot'] > $maxSlot) $maxSlot = $ds['slot']; 433*8e9c470bSAtari911 } 43419378907SAtari911 435*8e9c470bSAtari911 $slotMap = array(); 436*8e9c470bSAtari911 foreach ($eventRanges[$dateKey] as $evt) { 437*8e9c470bSAtari911 $eid = isset($evt['id']) ? $evt['id'] : $evt['title']; 438*8e9c470bSAtari911 if (isset($eventSlots[$eid])) { 439*8e9c470bSAtari911 $slotMap[$eventSlots[$eid]] = $evt; 440*8e9c470bSAtari911 } 441*8e9c470bSAtari911 } 44219378907SAtari911 44319378907SAtari911 $html .= '<div class="event-indicators">'; 444*8e9c470bSAtari911 for ($s = 0; $s <= $maxSlot; $s++) { 445*8e9c470bSAtari911 if (!isset($slotMap[$s])) { 446*8e9c470bSAtari911 // Spacer 447*8e9c470bSAtari911 $html .= '<span style="display:block;width:100%;height:6px;min-height:6px;flex-shrink:0;"></span>'; 448*8e9c470bSAtari911 continue; 449*8e9c470bSAtari911 } 450*8e9c470bSAtari911 451*8e9c470bSAtari911 $evt = $slotMap[$s]; 45219378907SAtari911 $eventId = isset($evt['id']) ? $evt['id'] : ''; 453*8e9c470bSAtari911 $eventColor = isset($evt['color']) ? hsc($evt['color']) : '#3498db'; 45419378907SAtari911 $eventTime = isset($evt['time']) ? $evt['time'] : ''; 455*8e9c470bSAtari911 $eventTitle = isset($evt['title']) ? hsc($evt['title']) : 'Event'; 45687ac9bf3SAtari911 $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey; 45787ac9bf3SAtari911 $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true; 45887ac9bf3SAtari911 $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true; 45919378907SAtari911 46096df7d3eSAtari911 $evtNs = isset($evt['namespace']) ? $evt['namespace'] : ''; 461*8e9c470bSAtari911 if (!$evtNs && isset($evt['_namespace'])) $evtNs = $evt['_namespace']; 46296df7d3eSAtari911 $isImportantEvent = false; 46396df7d3eSAtari911 foreach ($importantNsList as $impNs) { 46496df7d3eSAtari911 if ($evtNs === $impNs || strpos($evtNs, $impNs . ':') === 0) { 46596df7d3eSAtari911 $isImportantEvent = true; 46696df7d3eSAtari911 break; 46796df7d3eSAtari911 } 46896df7d3eSAtari911 } 46996df7d3eSAtari911 47019378907SAtari911 $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed'; 47187ac9bf3SAtari911 if (!$isFirstDay) $barClass .= ' event-bar-continues'; 47287ac9bf3SAtari911 if (!$isLastDay) $barClass .= ' event-bar-continuing'; 47396df7d3eSAtari911 if ($isImportantEvent) { 47496df7d3eSAtari911 $barClass .= ' event-bar-important'; 475*8e9c470bSAtari911 if ($isFirstDay) $barClass .= ' event-bar-has-star'; 47696df7d3eSAtari911 } 47796df7d3eSAtari911 47896df7d3eSAtari911 $titlePrefix = $isImportantEvent ? '⭐ ' : ''; 47987ac9bf3SAtari911 48019378907SAtari911 $html .= '<span class="event-bar ' . $barClass . '" '; 48119378907SAtari911 $html .= 'style="background: ' . $eventColor . ';" '; 48296df7d3eSAtari911 $html .= 'title="' . $titlePrefix . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" '; 48387ac9bf3SAtari911 $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">'; 48419378907SAtari911 $html .= '</span>'; 48519378907SAtari911 } 48619378907SAtari911 $html .= '</div>'; 48719378907SAtari911 } 48819378907SAtari911 48919378907SAtari911 $html .= '</td>'; 49019378907SAtari911 $currentDay++; 49119378907SAtari911 } 49219378907SAtari911 } 49319378907SAtari911 $html .= '</tr>'; 49419378907SAtari911 } 49519378907SAtari911 49619378907SAtari911 $html .= '</tbody></table>'; 49719378907SAtari911 $html .= '</div>'; // End calendar-left 49819378907SAtari911 49919378907SAtari911 // Right side: Event list 50019378907SAtari911 $html .= '<div class="calendar-compact-right">'; 50119378907SAtari911 $html .= '<div class="event-list-header">'; 50219378907SAtari911 $html .= '<div class="event-list-header-content">'; 503da206178SAtari911 $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>'; 50419378907SAtari911 if ($namespace) { 50519378907SAtari911 $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>'; 50619378907SAtari911 } 50719378907SAtari911 $html .= '</div>'; 5081d05cddcSAtari911 5091d05cddcSAtari911 // Search bar in header 51064a96c92SAtari911 $searchDefault = $this->getSearchDefault(); 51164a96c92SAtari911 $searchAllClass = $searchDefault === 'all' ? ' all-dates' : ''; 51264a96c92SAtari911 $searchIcon = $searchDefault === 'all' ? '' : ''; 51364a96c92SAtari911 $searchTitle = $searchDefault === 'all' ? 'Searching all dates' : 'Search this month only'; 51464a96c92SAtari911 $searchPlaceholder = $searchDefault === 'all' ? 'Search all dates...' : ' Search...'; 5151d05cddcSAtari911 $html .= '<div class="event-search-container-inline">'; 51664a96c92SAtari911 $html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" placeholder="' . htmlspecialchars($searchPlaceholder) . '" oninput="filterEvents(\'' . $calId . '\', this.value)">'; 5171d05cddcSAtari911 $html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>'; 51864a96c92SAtari911 $html .= '<button class="event-search-mode-inline' . $searchAllClass . '" id="search-mode-' . $calId . '" onclick="toggleSearchMode(\'' . $calId . '\', \'' . $namespace . '\')" title="' . htmlspecialchars($searchTitle) . '">' . $searchIcon . '</button>'; 5191d05cddcSAtari911 $html .= '</div>'; 5201d05cddcSAtari911 521da206178SAtari911 $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>'; 52219378907SAtari911 $html .= '</div>'; 52319378907SAtari911 52419378907SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">'; 5259ccd446eSAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace, $themeStyles); 52619378907SAtari911 $html .= '</div>'; 52719378907SAtari911 52819378907SAtari911 $html .= '</div>'; // End calendar-right 52919378907SAtari911 53019378907SAtari911 // Event dialog 5310c3b6e81SAtari911 $html .= $this->renderEventDialog($calId, $namespace, $theme); 53219378907SAtari911 53387ac9bf3SAtari911 // Month/Year picker dialog (at container level for proper overlay) 5349ccd446eSAtari911 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles); 53587ac9bf3SAtari911 53619378907SAtari911 $html .= '</div>'; // End container 53719378907SAtari911 53819378907SAtari911 return $html; 53919378907SAtari911 } 54019378907SAtari911 541da206178SAtari911 /** 542da206178SAtari911 * Render a static/read-only calendar for presentation and printing 543da206178SAtari911 * No edit buttons, clean layout, print-friendly itinerary 544da206178SAtari911 */ 545da206178SAtari911 private function renderStaticCalendar($data) { 546da206178SAtari911 $year = (int)$data['year']; 547da206178SAtari911 $month = (int)$data['month']; 548da206178SAtari911 $namespace = isset($data['namespace']) ? $data['namespace'] : ''; 5492866e827SAtari911 $exclude = isset($data['exclude']) ? $data['exclude'] : ''; 5502866e827SAtari911 $excludeList = $this->parseExcludeList($exclude); 551da206178SAtari911 $customTitle = isset($data['title']) ? $data['title'] : ''; 552da206178SAtari911 $noprint = isset($data['noprint']) && $data['noprint']; 553da206178SAtari911 $locked = isset($data['locked']) && $data['locked']; 554da206178SAtari911 $themeOverride = isset($data['theme']) ? $data['theme'] : ''; 555da206178SAtari911 556da206178SAtari911 // Generate unique ID for this static calendar 557da206178SAtari911 $calId = 'static-cal-' . substr(md5($namespace . $year . $month . uniqid()), 0, 8); 558da206178SAtari911 559da206178SAtari911 // Get theme settings 560da206178SAtari911 if ($themeOverride && in_array($themeOverride, ['matrix', 'pink', 'purple', 'professional', 'wiki', 'dark', 'light'])) { 561da206178SAtari911 $theme = $themeOverride; 562da206178SAtari911 } else { 563da206178SAtari911 $theme = $this->getSidebarTheme(); 564da206178SAtari911 } 565da206178SAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 566da206178SAtari911 567da206178SAtari911 // Get important namespaces 568da206178SAtari911 $importantNsList = $this->getImportantNamespaces(); 569da206178SAtari911 570da206178SAtari911 // Load events - check for multi-namespace or wildcard 571da206178SAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 572da206178SAtari911 if ($isMultiNamespace) { 5732866e827SAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList); 574da206178SAtari911 } else { 575da206178SAtari911 $events = $this->loadEvents($namespace, $year, $month); 576da206178SAtari911 } 577da206178SAtari911 578da206178SAtari911 // Month info 579da206178SAtari911 $firstDay = mktime(0, 0, 0, $month, 1, $year); 580da206178SAtari911 $daysInMonth = date('t', $firstDay); 581da206178SAtari911 $startDayOfWeek = (int)date('w', $firstDay); 582da206178SAtari911 $monthName = date('F', $firstDay); 583da206178SAtari911 584da206178SAtari911 // Display title - custom or default month/year 585da206178SAtari911 $displayTitle = $customTitle ? $customTitle : $monthName . ' ' . $year; 586da206178SAtari911 587da206178SAtari911 // Theme class for styling 588da206178SAtari911 $themeClass = 'static-theme-' . $theme; 589da206178SAtari911 590da206178SAtari911 // Build HTML 5912866e827SAtari911 $html = '<div class="calendar-static ' . $themeClass . '" id="' . $calId . '" data-year="' . $year . '" data-month="' . $month . '" data-namespace="' . hsc($namespace) . '" data-exclude="' . hsc($exclude) . '" data-locked="' . ($locked ? '1' : '0') . '">'; 592da206178SAtari911 593da206178SAtari911 // Screen view: Calendar Grid 594da206178SAtari911 $html .= '<div class="static-screen-view">'; 595da206178SAtari911 596da206178SAtari911 // Header with navigation (hide nav buttons if locked) 597da206178SAtari911 $html .= '<div class="static-header">'; 598da206178SAtari911 if (!$locked) { 599da206178SAtari911 $html .= '<button class="static-nav-btn" onclick="navStaticCalendar(\'' . $calId . '\', -1)" title="' . $this->getLang('previous_month') . '">◀</button>'; 600da206178SAtari911 } 601da206178SAtari911 $html .= '<h2 class="static-month-title">' . hsc($displayTitle) . '</h2>'; 602da206178SAtari911 if (!$locked) { 603da206178SAtari911 $html .= '<button class="static-nav-btn" onclick="navStaticCalendar(\'' . $calId . '\', 1)" title="' . $this->getLang('next_month') . '">▶</button>'; 604da206178SAtari911 } 605da206178SAtari911 if (!$noprint) { 606da206178SAtari911 $html .= '<button class="static-print-btn" onclick="printStaticCalendar(\'' . $calId . '\')" title="' . $this->getLang('print_calendar') . '">️</button>'; 607da206178SAtari911 } 608da206178SAtari911 $html .= '</div>'; 609da206178SAtari911 610da206178SAtari911 // Calendar grid 611da206178SAtari911 $html .= '<table class="static-calendar-grid">'; 612da206178SAtari911 $html .= '<thead><tr>'; 613da206178SAtari911 $dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 614da206178SAtari911 foreach ($dayNames as $day) { 615da206178SAtari911 $html .= '<th>' . $day . '</th>'; 616da206178SAtari911 } 617da206178SAtari911 $html .= '</tr></thead>'; 618da206178SAtari911 $html .= '<tbody>'; 619da206178SAtari911 620da206178SAtari911 $dayCount = 1; 621da206178SAtari911 $totalCells = $startDayOfWeek + $daysInMonth; 622da206178SAtari911 $rows = ceil($totalCells / 7); 623da206178SAtari911 624da206178SAtari911 for ($row = 0; $row < $rows; $row++) { 625da206178SAtari911 $html .= '<tr>'; 626da206178SAtari911 for ($col = 0; $col < 7; $col++) { 627da206178SAtari911 $cellNum = $row * 7 + $col; 628da206178SAtari911 629da206178SAtari911 if ($cellNum < $startDayOfWeek || $dayCount > $daysInMonth) { 630da206178SAtari911 $html .= '<td class="static-day-empty"></td>'; 631da206178SAtari911 } else { 632da206178SAtari911 $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $dayCount); 633da206178SAtari911 $dayEvents = isset($events[$dateKey]) ? $events[$dateKey] : []; 634da206178SAtari911 $isToday = ($dateKey === date('Y-m-d')); 635da206178SAtari911 $isWeekend = ($col === 0 || $col === 6); 636da206178SAtari911 637da206178SAtari911 $cellClass = 'static-day'; 638da206178SAtari911 if ($isToday) $cellClass .= ' static-day-today'; 639da206178SAtari911 if ($isWeekend) $cellClass .= ' static-day-weekend'; 640da206178SAtari911 if (!empty($dayEvents)) $cellClass .= ' static-day-has-events'; 641da206178SAtari911 642da206178SAtari911 $html .= '<td class="' . $cellClass . '">'; 643da206178SAtari911 $html .= '<div class="static-day-number">' . $dayCount . '</div>'; 644da206178SAtari911 645da206178SAtari911 if (!empty($dayEvents)) { 646da206178SAtari911 $html .= '<div class="static-day-events">'; 647da206178SAtari911 foreach ($dayEvents as $event) { 648da206178SAtari911 $color = isset($event['color']) ? $event['color'] : '#3498db'; 649da206178SAtari911 $title = hsc($event['title']); 650da206178SAtari911 $time = isset($event['time']) && $event['time'] ? $event['time'] : ''; 651da206178SAtari911 $desc = isset($event['description']) ? $event['description'] : ''; 652da206178SAtari911 $eventNs = isset($event['namespace']) ? $event['namespace'] : $namespace; 653da206178SAtari911 654da206178SAtari911 // Check if important 655da206178SAtari911 $isImportant = false; 656da206178SAtari911 foreach ($importantNsList as $impNs) { 657da206178SAtari911 if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) { 658da206178SAtari911 $isImportant = true; 659da206178SAtari911 break; 660da206178SAtari911 } 661da206178SAtari911 } 662da206178SAtari911 663da206178SAtari911 // Build tooltip - plain text with basic formatting indicators 664da206178SAtari911 $tooltipText = $event['title']; 665da206178SAtari911 if ($time) { 666da206178SAtari911 $tooltipText .= "\n " . $this->formatTime12Hour($time); 667da206178SAtari911 if (isset($event['endTime']) && $event['endTime']) { 668da206178SAtari911 $tooltipText .= ' - ' . $this->formatTime12Hour($event['endTime']); 669da206178SAtari911 } 670da206178SAtari911 } 671da206178SAtari911 if ($desc) { 672da206178SAtari911 // Convert formatting to plain text equivalents 673da206178SAtari911 $plainDesc = $desc; 674da206178SAtari911 $plainDesc = preg_replace('/\*\*(.+?)\*\*/', '*$1*', $plainDesc); 675da206178SAtari911 $plainDesc = preg_replace('/__(.+?)__/', '*$1*', $plainDesc); 676da206178SAtari911 $plainDesc = preg_replace('/\/\/(.+?)\/\//', '_$1_', $plainDesc); 677da206178SAtari911 $plainDesc = preg_replace('/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/', '$2 ($1)', $plainDesc); 678da206178SAtari911 $plainDesc = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1 ($2)', $plainDesc); 679da206178SAtari911 $tooltipText .= "\n\n" . $plainDesc; 680da206178SAtari911 } 681da206178SAtari911 682da206178SAtari911 $eventClass = 'static-event'; 683da206178SAtari911 if ($isImportant) $eventClass .= ' static-event-important'; 684da206178SAtari911 685da206178SAtari911 $html .= '<div class="' . $eventClass . '" style="border-left-color: ' . $color . ';" title="' . hsc($tooltipText) . '">'; 686da206178SAtari911 if ($isImportant) { 687da206178SAtari911 $html .= '<span class="static-event-star">⭐</span>'; 688da206178SAtari911 } 689da206178SAtari911 if ($time) { 690da206178SAtari911 $html .= '<span class="static-event-time">' . $this->formatTime12Hour($time) . '</span> '; 691da206178SAtari911 } 692da206178SAtari911 $html .= '<span class="static-event-title">' . $title . '</span>'; 693da206178SAtari911 $html .= '</div>'; 694da206178SAtari911 } 695da206178SAtari911 $html .= '</div>'; 696da206178SAtari911 } 697da206178SAtari911 698da206178SAtari911 $html .= '</td>'; 699da206178SAtari911 $dayCount++; 700da206178SAtari911 } 701da206178SAtari911 } 702da206178SAtari911 $html .= '</tr>'; 703da206178SAtari911 } 704da206178SAtari911 705da206178SAtari911 $html .= '</tbody></table>'; 706da206178SAtari911 $html .= '</div>'; // End screen view 707da206178SAtari911 708da206178SAtari911 // Print view: Itinerary format (skip if noprint) 709da206178SAtari911 if (!$noprint) { 710da206178SAtari911 $html .= '<div class="static-print-view">'; 711da206178SAtari911 $html .= '<h2 class="static-print-title">' . hsc($displayTitle) . '</h2>'; 712da206178SAtari911 713da206178SAtari911 if (!empty($namespace)) { 714da206178SAtari911 $html .= '<p class="static-print-namespace">' . $this->getLang('calendar_label') . ': ' . hsc($namespace) . '</p>'; 715da206178SAtari911 } 716da206178SAtari911 717da206178SAtari911 // Collect all events sorted by date 718da206178SAtari911 $allEvents = []; 719da206178SAtari911 foreach ($events as $dateKey => $dayEvents) { 720da206178SAtari911 foreach ($dayEvents as $event) { 721da206178SAtari911 $event['_date'] = $dateKey; 722da206178SAtari911 $allEvents[] = $event; 723da206178SAtari911 } 724da206178SAtari911 } 725da206178SAtari911 726da206178SAtari911 // Sort by date, then time 727da206178SAtari911 usort($allEvents, function($a, $b) { 728da206178SAtari911 $dateCompare = strcmp($a['_date'], $b['_date']); 729da206178SAtari911 if ($dateCompare !== 0) return $dateCompare; 730da206178SAtari911 $timeA = isset($a['time']) ? $a['time'] : '99:99'; 731da206178SAtari911 $timeB = isset($b['time']) ? $b['time'] : '99:99'; 732da206178SAtari911 return strcmp($timeA, $timeB); 733da206178SAtari911 }); 734da206178SAtari911 735da206178SAtari911 if (empty($allEvents)) { 736da206178SAtari911 $html .= '<p class="static-print-empty">' . $this->getLang('no_events_scheduled') . '</p>'; 737da206178SAtari911 } else { 738da206178SAtari911 $html .= '<table class="static-itinerary">'; 739da206178SAtari911 $html .= '<thead><tr><th>Date</th><th>Time</th><th>Event</th><th>Details</th></tr></thead>'; 740da206178SAtari911 $html .= '<tbody>'; 741da206178SAtari911 742da206178SAtari911 $lastDate = ''; 743da206178SAtari911 foreach ($allEvents as $event) { 744da206178SAtari911 $dateKey = $event['_date']; 745da206178SAtari911 $dateObj = new \DateTime($dateKey); 746da206178SAtari911 $dateDisplay = $dateObj->format('D, M j'); 747da206178SAtari911 $eventNs = isset($event['namespace']) ? $event['namespace'] : $namespace; 748da206178SAtari911 749da206178SAtari911 // Check if important 750da206178SAtari911 $isImportant = false; 751da206178SAtari911 foreach ($importantNsList as $impNs) { 752da206178SAtari911 if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) { 753da206178SAtari911 $isImportant = true; 754da206178SAtari911 break; 755da206178SAtari911 } 756da206178SAtari911 } 757da206178SAtari911 758da206178SAtari911 $rowClass = $isImportant ? 'static-itinerary-important' : ''; 759da206178SAtari911 760da206178SAtari911 $html .= '<tr class="' . $rowClass . '">'; 761da206178SAtari911 762da206178SAtari911 // Only show date if different from previous row 763da206178SAtari911 if ($dateKey !== $lastDate) { 764da206178SAtari911 $html .= '<td class="static-itinerary-date">' . $dateDisplay . '</td>'; 765da206178SAtari911 $lastDate = $dateKey; 766da206178SAtari911 } else { 767da206178SAtari911 $html .= '<td></td>'; 768da206178SAtari911 } 769da206178SAtari911 770da206178SAtari911 // Time 771da206178SAtari911 $time = isset($event['time']) && $event['time'] ? $this->formatTime12Hour($event['time']) : $this->getLang('all_day'); 772da206178SAtari911 if (isset($event['endTime']) && $event['endTime'] && isset($event['time']) && $event['time']) { 773da206178SAtari911 $time .= ' - ' . $this->formatTime12Hour($event['endTime']); 774da206178SAtari911 } 775da206178SAtari911 $html .= '<td class="static-itinerary-time">' . $time . '</td>'; 776da206178SAtari911 777da206178SAtari911 // Title with star for important 778da206178SAtari911 $html .= '<td class="static-itinerary-title">'; 779da206178SAtari911 if ($isImportant) { 780da206178SAtari911 $html .= '⭐ '; 781da206178SAtari911 } 782da206178SAtari911 $html .= hsc($event['title']); 783da206178SAtari911 $html .= '</td>'; 784da206178SAtari911 785da206178SAtari911 // Description - with formatting 786da206178SAtari911 $desc = isset($event['description']) ? $this->renderDescription($event['description']) : ''; 787da206178SAtari911 $html .= '<td class="static-itinerary-desc">' . $desc . '</td>'; 788da206178SAtari911 789da206178SAtari911 $html .= '</tr>'; 790da206178SAtari911 } 791da206178SAtari911 792da206178SAtari911 $html .= '</tbody></table>'; 793da206178SAtari911 } 794da206178SAtari911 795da206178SAtari911 $html .= '</div>'; // End print view 796da206178SAtari911 } // End noprint check 797da206178SAtari911 798da206178SAtari911 $html .= '</div>'; // End container 799da206178SAtari911 800da206178SAtari911 return $html; 801da206178SAtari911 } 802da206178SAtari911 803da206178SAtari911 /** 804da206178SAtari911 * Format time to 12-hour format 805da206178SAtari911 */ 806da206178SAtari911 private function formatTime12Hour($time) { 807da206178SAtari911 if (!$time) return ''; 808da206178SAtari911 $parts = explode(':', $time); 809da206178SAtari911 $hour = (int)$parts[0]; 810da206178SAtari911 $minute = isset($parts[1]) ? $parts[1] : '00'; 811da206178SAtari911 $ampm = $hour >= 12 ? 'PM' : 'AM'; 812da206178SAtari911 $hour12 = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour); 813da206178SAtari911 return $hour12 . ':' . $minute . ' ' . $ampm; 814da206178SAtari911 } 815da206178SAtari911 816da206178SAtari911 /** 817da206178SAtari911 * Get list of important namespaces from config 818da206178SAtari911 */ 819da206178SAtari911 private function getImportantNamespaces() { 8202866e827SAtari911 $configFile = $this->syncConfigPath(); 821da206178SAtari911 $importantNsList = ['important']; // default 822da206178SAtari911 if (file_exists($configFile)) { 823da206178SAtari911 $config = include $configFile; 824da206178SAtari911 if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) { 825da206178SAtari911 $importantNsList = array_map('trim', explode(',', $config['important_namespaces'])); 826da206178SAtari911 } 827da206178SAtari911 } 828da206178SAtari911 return $importantNsList; 829da206178SAtari911 } 830da206178SAtari911 8319ccd446eSAtari911 private function renderEventListContent($events, $calId, $namespace, $themeStyles = null) { 83219378907SAtari911 if (empty($events)) { 83319378907SAtari911 return '<p class="no-events-msg">No events this month</p>'; 83419378907SAtari911 } 83519378907SAtari911 8369ccd446eSAtari911 // Default theme styles if not provided 8379ccd446eSAtari911 if ($themeStyles === null) { 8389ccd446eSAtari911 $theme = $this->getSidebarTheme(); 8399ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 84096df7d3eSAtari911 } else { 84196df7d3eSAtari911 $theme = $this->getSidebarTheme(); 84296df7d3eSAtari911 } 84396df7d3eSAtari911 84496df7d3eSAtari911 // Get important namespaces from config 8452866e827SAtari911 $configFile = $this->syncConfigPath(); 84696df7d3eSAtari911 $importantNsList = ['important']; // default 84796df7d3eSAtari911 if (file_exists($configFile)) { 84896df7d3eSAtari911 $config = include $configFile; 84996df7d3eSAtari911 if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) { 85096df7d3eSAtari911 $importantNsList = array_map('trim', explode(',', $config['important_namespaces'])); 85196df7d3eSAtari911 } 8529ccd446eSAtari911 } 8539ccd446eSAtari911 8541d05cddcSAtari911 // Check for time conflicts 8551d05cddcSAtari911 $events = $this->checkTimeConflicts($events); 8561d05cddcSAtari911 857e3a9f44cSAtari911 // Sort by date ascending (chronological order - oldest first) 85819378907SAtari911 ksort($events); 85919378907SAtari911 860e3a9f44cSAtari911 // Sort events within each day by time 861e3a9f44cSAtari911 foreach ($events as $dateKey => &$dayEvents) { 862e3a9f44cSAtari911 usort($dayEvents, function($a, $b) { 8631d05cddcSAtari911 $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null; 8641d05cddcSAtari911 $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null; 8651d05cddcSAtari911 8661d05cddcSAtari911 // All-day events (no time) go to the TOP 8671d05cddcSAtari911 if ($timeA === null && $timeB !== null) return -1; // A before B 8681d05cddcSAtari911 if ($timeA !== null && $timeB === null) return 1; // A after B 8691d05cddcSAtari911 if ($timeA === null && $timeB === null) return 0; // Both all-day, equal 8701d05cddcSAtari911 8711d05cddcSAtari911 // Both have times, sort chronologically 872e3a9f44cSAtari911 return strcmp($timeA, $timeB); 873e3a9f44cSAtari911 }); 874e3a9f44cSAtari911 } 875e3a9f44cSAtari911 unset($dayEvents); // Break reference 876e3a9f44cSAtari911 877e3a9f44cSAtari911 // Get today's date for comparison 878e3a9f44cSAtari911 $today = date('Y-m-d'); 879e3a9f44cSAtari911 $firstFutureEventId = null; 880e3a9f44cSAtari911 8811d05cddcSAtari911 // Helper function to check if event is past (with 15-minute grace period for timed events) 8821d05cddcSAtari911 $isEventPast = function($dateKey, $time) use ($today) { 8831d05cddcSAtari911 // If event is on a past date, it's definitely past 8841d05cddcSAtari911 if ($dateKey < $today) { 8851d05cddcSAtari911 return true; 8861d05cddcSAtari911 } 8871d05cddcSAtari911 8881d05cddcSAtari911 // If event is on a future date, it's definitely not past 8891d05cddcSAtari911 if ($dateKey > $today) { 8901d05cddcSAtari911 return false; 8911d05cddcSAtari911 } 8921d05cddcSAtari911 8931d05cddcSAtari911 // Event is today - check time with grace period 8941d05cddcSAtari911 if ($time && $time !== '') { 8951d05cddcSAtari911 try { 8961d05cddcSAtari911 $currentDateTime = new DateTime(); 8971d05cddcSAtari911 $eventDateTime = new DateTime($dateKey . ' ' . $time); 8981d05cddcSAtari911 8991d05cddcSAtari911 // Add 15-minute grace period 9001d05cddcSAtari911 $eventDateTime->modify('+15 minutes'); 9011d05cddcSAtari911 9021d05cddcSAtari911 // Event is past if current time > event time + 15 minutes 9031d05cddcSAtari911 return $currentDateTime > $eventDateTime; 9041d05cddcSAtari911 } catch (Exception $e) { 9051d05cddcSAtari911 // If time parsing fails, fall back to date-only comparison 9061d05cddcSAtari911 return false; 9071d05cddcSAtari911 } 9081d05cddcSAtari911 } 9091d05cddcSAtari911 9101d05cddcSAtari911 // No time specified for today's event, treat as future 9111d05cddcSAtari911 return false; 9121d05cddcSAtari911 }; 9131d05cddcSAtari911 9141d05cddcSAtari911 // Build HTML for each event - separate past/completed from future 9151d05cddcSAtari911 $pastHtml = ''; 9161d05cddcSAtari911 $futureHtml = ''; 9171d05cddcSAtari911 $pastCount = 0; 918e3a9f44cSAtari911 91919378907SAtari911 foreach ($events as $dateKey => $dayEvents) { 920e3a9f44cSAtari911 92119378907SAtari911 foreach ($dayEvents as $event) { 922e3a9f44cSAtari911 // Track first future/today event for auto-scroll 923e3a9f44cSAtari911 if (!$firstFutureEventId && $dateKey >= $today) { 924e3a9f44cSAtari911 $firstFutureEventId = isset($event['id']) ? $event['id'] : ''; 925e3a9f44cSAtari911 } 92619378907SAtari911 $eventId = isset($event['id']) ? $event['id'] : ''; 92719378907SAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 9281d05cddcSAtari911 $timeRaw = isset($event['time']) ? $event['time'] : ''; 9291d05cddcSAtari911 $time = htmlspecialchars($timeRaw); 9301d05cddcSAtari911 $endTime = isset($event['endTime']) ? htmlspecialchars($event['endTime']) : ''; 93119378907SAtari911 $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db'; 93219378907SAtari911 $description = isset($event['description']) ? $event['description'] : ''; 93319378907SAtari911 $isTask = isset($event['isTask']) ? $event['isTask'] : false; 93419378907SAtari911 $completed = isset($event['completed']) ? $event['completed'] : false; 93519378907SAtari911 $endDate = isset($event['endDate']) ? $event['endDate'] : ''; 93619378907SAtari911 9371d05cddcSAtari911 // Use helper function to determine if event is past (with grace period) 9381d05cddcSAtari911 $isPast = $isEventPast($dateKey, $timeRaw); 9391d05cddcSAtari911 $isToday = $dateKey === $today; 9401d05cddcSAtari911 9411d05cddcSAtari911 // Check if event should be in past section 9421d05cddcSAtari911 // EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past 9431d05cddcSAtari911 $isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed; 9441d05cddcSAtari911 if ($isPastOrCompleted) { 9451d05cddcSAtari911 $pastCount++; 9461d05cddcSAtari911 } 9471d05cddcSAtari911 9481d05cddcSAtari911 // Determine if task is past due (past date, is task, not completed) 9491d05cddcSAtari911 $isPastDue = $isPast && $isTask && !$completed; 9501d05cddcSAtari911 95119378907SAtari911 // Process description for wiki syntax, HTML, images, and links 9529ccd446eSAtari911 $renderedDescription = $this->renderDescription($description, $themeStyles); 95319378907SAtari911 9541d05cddcSAtari911 // Convert to 12-hour format and handle time ranges 95519378907SAtari911 $displayTime = ''; 95619378907SAtari911 if ($time) { 95719378907SAtari911 $timeObj = DateTime::createFromFormat('H:i', $time); 95819378907SAtari911 if ($timeObj) { 95919378907SAtari911 $displayTime = $timeObj->format('g:i A'); 9601d05cddcSAtari911 9611d05cddcSAtari911 // Add end time if present and different from start time 9621d05cddcSAtari911 if ($endTime && $endTime !== $time) { 9631d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $endTime); 9641d05cddcSAtari911 if ($endTimeObj) { 9651d05cddcSAtari911 $displayTime .= ' - ' . $endTimeObj->format('g:i A'); 9661d05cddcSAtari911 } 9671d05cddcSAtari911 } 96819378907SAtari911 } else { 96919378907SAtari911 $displayTime = $time; 97019378907SAtari911 } 97119378907SAtari911 } 97219378907SAtari911 97387ac9bf3SAtari911 // Format date display with day of week 974e3a9f44cSAtari911 // Use originalStartDate if this is a multi-month event continuation 975e3a9f44cSAtari911 $displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey; 976e3a9f44cSAtari911 $dateObj = new DateTime($displayDateKey); 97787ac9bf3SAtari911 $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24" 97819378907SAtari911 97919378907SAtari911 // Multi-day indicator 98019378907SAtari911 $multiDay = ''; 981e3a9f44cSAtari911 if ($endDate && $endDate !== $displayDateKey) { 98219378907SAtari911 $endObj = new DateTime($endDate); 98387ac9bf3SAtari911 $multiDay = ' → ' . $endObj->format('D, M j'); 98419378907SAtari911 } 98519378907SAtari911 98619378907SAtari911 $completedClass = $completed ? ' event-completed' : ''; 9871d05cddcSAtari911 // Don't grey out past due tasks - they need attention! 9881d05cddcSAtari911 $pastClass = ($isPast && !$isPastDue) ? ' event-past' : ''; 9891d05cddcSAtari911 $pastDueClass = $isPastDue ? ' event-pastdue' : ''; 990e3a9f44cSAtari911 $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : ''; 99119378907SAtari911 99296df7d3eSAtari911 // Check if this is an important namespace event 99396df7d3eSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 99496df7d3eSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 99596df7d3eSAtari911 $eventNamespace = $event['_namespace']; 99696df7d3eSAtari911 } 99796df7d3eSAtari911 $isImportantNs = false; 99896df7d3eSAtari911 foreach ($importantNsList as $impNs) { 99996df7d3eSAtari911 if ($eventNamespace === $impNs || strpos($eventNamespace, $impNs . ':') === 0) { 100096df7d3eSAtari911 $isImportantNs = true; 100196df7d3eSAtari911 break; 100296df7d3eSAtari911 } 100396df7d3eSAtari911 } 100496df7d3eSAtari911 $importantClass = $isImportantNs ? ' event-important' : ''; 100596df7d3eSAtari911 10069ccd446eSAtari911 // For all themes: use CSS variables, only keep border-left-color as inline 10079ccd446eSAtari911 $pastClickHandler = ($isPast && !$isPastDue) ? ' onclick="togglePastEventExpand(this)"' : ''; 100896df7d3eSAtari911 $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 . '>'; 10091d05cddcSAtari911 $eventHtml .= '<div class="event-info">'; 10109ccd446eSAtari911 10111d05cddcSAtari911 $eventHtml .= '<div class="event-title-row">'; 101296df7d3eSAtari911 // Add star for important namespace events 101396df7d3eSAtari911 if ($isImportantNs) { 1014da206178SAtari911 $eventHtml .= '<span class="event-important-star" title="Important">⭐</span> '; 101596df7d3eSAtari911 } 10161d05cddcSAtari911 $eventHtml .= '<span class="event-title-compact">' . $title . '</span>'; 10171d05cddcSAtari911 $eventHtml .= '</div>'; 101819378907SAtari911 1019e3a9f44cSAtari911 // For past events, hide meta and description (collapsed) 10201d05cddcSAtari911 // EXCEPTION: Past due tasks should show their details 10211d05cddcSAtari911 if (!$isPast || $isPastDue) { 10221d05cddcSAtari911 $eventHtml .= '<div class="event-meta-compact">'; 10231d05cddcSAtari911 $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay; 102419378907SAtari911 if ($displayTime) { 10251d05cddcSAtari911 $eventHtml .= ' • ' . $displayTime; 102619378907SAtari911 } 10271d05cddcSAtari911 // Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks 10281d05cddcSAtari911 if ($isPastDue) { 10297e8ea635SAtari911 $eventHtml .= ' <span class="event-pastdue-badge" style="background:' . $themeStyles['pastdue_color'] . ' !important; color:white !important; -webkit-text-fill-color:white !important;">' . 'PAST DUE</span>'; 10301d05cddcSAtari911 } elseif ($isToday) { 10317e8ea635SAtari911 $eventHtml .= ' <span class="event-today-badge" style="background:' . $themeStyles['border'] . ' !important; color:' . $themeStyles['bg'] . ' !important; -webkit-text-fill-color:' . $themeStyles['bg'] . ' !important;">' . 'TODAY</span>'; 1032e3a9f44cSAtari911 } 10331d05cddcSAtari911 // Add namespace badge - ALWAYS show if event has a namespace 1034e3a9f44cSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 1035e3a9f44cSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 1036e3a9f44cSAtari911 $eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility 1037e3a9f44cSAtari911 } 10381d05cddcSAtari911 // Show badge if namespace exists and is not empty 10391d05cddcSAtari911 if ($eventNamespace && $eventNamespace !== '') { 10407e8ea635SAtari911 $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>'; 1041e3a9f44cSAtari911 } 10421d05cddcSAtari911 10431d05cddcSAtari911 // Add conflict warning if event has time conflicts 10441d05cddcSAtari911 if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) { 10451d05cddcSAtari911 $conflictList = []; 10461d05cddcSAtari911 foreach ($event['conflictsWith'] as $conflict) { 10479ccd446eSAtari911 $conflictText = $conflict['title']; 10481d05cddcSAtari911 if (!empty($conflict['time'])) { 10491d05cddcSAtari911 // Format time range 10501d05cddcSAtari911 $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']); 10511d05cddcSAtari911 $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time']; 10521d05cddcSAtari911 10531d05cddcSAtari911 if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) { 10541d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']); 10551d05cddcSAtari911 $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime']; 10561d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')'; 10571d05cddcSAtari911 } else { 10581d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ')'; 10591d05cddcSAtari911 } 10601d05cddcSAtari911 } 10611d05cddcSAtari911 $conflictList[] = $conflictText; 10621d05cddcSAtari911 } 10631d05cddcSAtari911 $conflictCount = count($event['conflictsWith']); 10649ccd446eSAtari911 $conflictJson = base64_encode(json_encode($conflictList)); 10651d05cddcSAtari911 $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>'; 10661d05cddcSAtari911 } 10671d05cddcSAtari911 10681d05cddcSAtari911 $eventHtml .= '</span>'; 10691d05cddcSAtari911 $eventHtml .= '</div>'; 107019378907SAtari911 107119378907SAtari911 if ($description) { 10721d05cddcSAtari911 $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>'; 10731d05cddcSAtari911 } 10741d05cddcSAtari911 } else { 10751d05cddcSAtari911 // Past events: render with display:none for click-to-expand 10761d05cddcSAtari911 $eventHtml .= '<div class="event-meta-compact" style="display:none;">'; 10771d05cddcSAtari911 $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay; 10781d05cddcSAtari911 if ($displayTime) { 10791d05cddcSAtari911 $eventHtml .= ' • ' . $displayTime; 10801d05cddcSAtari911 } 10811d05cddcSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 10821d05cddcSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 10831d05cddcSAtari911 $eventNamespace = $event['_namespace']; 10841d05cddcSAtari911 } 10851d05cddcSAtari911 if ($eventNamespace && $eventNamespace !== '') { 10867e8ea635SAtari911 $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>'; 10871d05cddcSAtari911 } 10881d05cddcSAtari911 10891d05cddcSAtari911 // Add conflict warning if event has time conflicts 10901d05cddcSAtari911 if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) { 10911d05cddcSAtari911 $conflictList = []; 10921d05cddcSAtari911 foreach ($event['conflictsWith'] as $conflict) { 10939ccd446eSAtari911 $conflictText = $conflict['title']; 10941d05cddcSAtari911 if (!empty($conflict['time'])) { 10951d05cddcSAtari911 $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']); 10961d05cddcSAtari911 $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time']; 10971d05cddcSAtari911 10981d05cddcSAtari911 if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) { 10991d05cddcSAtari911 $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']); 11001d05cddcSAtari911 $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime']; 11011d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')'; 11021d05cddcSAtari911 } else { 11031d05cddcSAtari911 $conflictText .= ' (' . $startTimeFormatted . ')'; 11041d05cddcSAtari911 } 11051d05cddcSAtari911 } 11061d05cddcSAtari911 $conflictList[] = $conflictText; 11071d05cddcSAtari911 } 11081d05cddcSAtari911 $conflictCount = count($event['conflictsWith']); 11099ccd446eSAtari911 $conflictJson = base64_encode(json_encode($conflictList)); 11101d05cddcSAtari911 $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>'; 11111d05cddcSAtari911 } 11121d05cddcSAtari911 11131d05cddcSAtari911 $eventHtml .= '</span>'; 11141d05cddcSAtari911 $eventHtml .= '</div>'; 11151d05cddcSAtari911 11161d05cddcSAtari911 if ($description) { 11171d05cddcSAtari911 $eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '</div>'; 111819378907SAtari911 } 1119e3a9f44cSAtari911 } 112019378907SAtari911 11211d05cddcSAtari911 $eventHtml .= '</div>'; // event-info 112219378907SAtari911 1123e3a9f44cSAtari911 // Use stored namespace from event, fallback to passed namespace 1124e3a9f44cSAtari911 $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace; 1125e3a9f44cSAtari911 11261d05cddcSAtari911 $eventHtml .= '<div class="event-actions-compact">'; 11271d05cddcSAtari911 $eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">️</button>'; 11281d05cddcSAtari911 $eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>'; 11291d05cddcSAtari911 $eventHtml .= '</div>'; 113019378907SAtari911 113119378907SAtari911 // Checkbox for tasks - ON THE FAR RIGHT 113219378907SAtari911 if ($isTask) { 113319378907SAtari911 $checked = $completed ? 'checked' : ''; 11341d05cddcSAtari911 $eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">'; 113519378907SAtari911 } 113619378907SAtari911 11371d05cddcSAtari911 $eventHtml .= '</div>'; 11381d05cddcSAtari911 11391d05cddcSAtari911 // Add to appropriate section 11401d05cddcSAtari911 if ($isPastOrCompleted) { 11411d05cddcSAtari911 $pastHtml .= $eventHtml; 11421d05cddcSAtari911 } else { 11431d05cddcSAtari911 $futureHtml .= $eventHtml; 11441d05cddcSAtari911 } 11451d05cddcSAtari911 } 11461d05cddcSAtari911 } 11471d05cddcSAtari911 11481d05cddcSAtari911 // Build final HTML with collapsible past events section 11491d05cddcSAtari911 $html = ''; 11501d05cddcSAtari911 11511d05cddcSAtari911 // Add collapsible past events section if any exist 11521d05cddcSAtari911 if ($pastCount > 0) { 11531d05cddcSAtari911 $html .= '<div class="past-events-section">'; 11541d05cddcSAtari911 $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">'; 11551d05cddcSAtari911 $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> '; 11561d05cddcSAtari911 $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>'; 115719378907SAtari911 $html .= '</div>'; 11581d05cddcSAtari911 $html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">'; 11591d05cddcSAtari911 $html .= $pastHtml; 11601d05cddcSAtari911 $html .= '</div>'; 11611d05cddcSAtari911 $html .= '</div>'; 11621d05cddcSAtari911 } 1163e3a9f44cSAtari911 11641d05cddcSAtari911 // Add future events 11651d05cddcSAtari911 $html .= $futureHtml; 116619378907SAtari911 116719378907SAtari911 return $html; 116819378907SAtari911 } 116919378907SAtari911 11701d05cddcSAtari911 /** 11711d05cddcSAtari911 * Check for time conflicts between events 11721d05cddcSAtari911 */ 11731d05cddcSAtari911 private function checkTimeConflicts($events) { 11741d05cddcSAtari911 // Group events by date 11751d05cddcSAtari911 $eventsByDate = []; 11761d05cddcSAtari911 foreach ($events as $date => $dateEvents) { 11771d05cddcSAtari911 if (!is_array($dateEvents)) continue; 11781d05cddcSAtari911 11791d05cddcSAtari911 foreach ($dateEvents as $evt) { 11801d05cddcSAtari911 if (empty($evt['time'])) continue; // Skip all-day events 11811d05cddcSAtari911 11821d05cddcSAtari911 if (!isset($eventsByDate[$date])) { 11831d05cddcSAtari911 $eventsByDate[$date] = []; 11841d05cddcSAtari911 } 11851d05cddcSAtari911 $eventsByDate[$date][] = $evt; 11861d05cddcSAtari911 } 11871d05cddcSAtari911 } 11881d05cddcSAtari911 11891d05cddcSAtari911 // Check for overlaps on each date 11901d05cddcSAtari911 foreach ($eventsByDate as $date => $dateEvents) { 11911d05cddcSAtari911 for ($i = 0; $i < count($dateEvents); $i++) { 11921d05cddcSAtari911 for ($j = $i + 1; $j < count($dateEvents); $j++) { 11931d05cddcSAtari911 if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) { 11941d05cddcSAtari911 // Mark both events as conflicting 11951d05cddcSAtari911 $dateEvents[$i]['hasConflict'] = true; 11961d05cddcSAtari911 $dateEvents[$j]['hasConflict'] = true; 11971d05cddcSAtari911 11981d05cddcSAtari911 // Store conflict info 11991d05cddcSAtari911 if (!isset($dateEvents[$i]['conflictsWith'])) { 12001d05cddcSAtari911 $dateEvents[$i]['conflictsWith'] = []; 12011d05cddcSAtari911 } 12021d05cddcSAtari911 if (!isset($dateEvents[$j]['conflictsWith'])) { 12031d05cddcSAtari911 $dateEvents[$j]['conflictsWith'] = []; 12041d05cddcSAtari911 } 12051d05cddcSAtari911 12061d05cddcSAtari911 $dateEvents[$i]['conflictsWith'][] = [ 12071d05cddcSAtari911 'id' => $dateEvents[$j]['id'], 12081d05cddcSAtari911 'title' => $dateEvents[$j]['title'], 12091d05cddcSAtari911 'time' => $dateEvents[$j]['time'], 12101d05cddcSAtari911 'endTime' => isset($dateEvents[$j]['endTime']) ? $dateEvents[$j]['endTime'] : '' 12111d05cddcSAtari911 ]; 12121d05cddcSAtari911 12131d05cddcSAtari911 $dateEvents[$j]['conflictsWith'][] = [ 12141d05cddcSAtari911 'id' => $dateEvents[$i]['id'], 12151d05cddcSAtari911 'title' => $dateEvents[$i]['title'], 12161d05cddcSAtari911 'time' => $dateEvents[$i]['time'], 12171d05cddcSAtari911 'endTime' => isset($dateEvents[$i]['endTime']) ? $dateEvents[$i]['endTime'] : '' 12181d05cddcSAtari911 ]; 12191d05cddcSAtari911 } 12201d05cddcSAtari911 } 12211d05cddcSAtari911 } 12221d05cddcSAtari911 12231d05cddcSAtari911 // Update the events array with conflict information 12241d05cddcSAtari911 foreach ($events[$date] as &$evt) { 12251d05cddcSAtari911 foreach ($dateEvents as $checkedEvt) { 12261d05cddcSAtari911 if ($evt['id'] === $checkedEvt['id']) { 12271d05cddcSAtari911 if (isset($checkedEvt['hasConflict'])) { 12281d05cddcSAtari911 $evt['hasConflict'] = $checkedEvt['hasConflict']; 12291d05cddcSAtari911 } 12301d05cddcSAtari911 if (isset($checkedEvt['conflictsWith'])) { 12311d05cddcSAtari911 $evt['conflictsWith'] = $checkedEvt['conflictsWith']; 12321d05cddcSAtari911 } 12331d05cddcSAtari911 break; 12341d05cddcSAtari911 } 12351d05cddcSAtari911 } 12361d05cddcSAtari911 } 12371d05cddcSAtari911 } 12381d05cddcSAtari911 12391d05cddcSAtari911 return $events; 12401d05cddcSAtari911 } 12411d05cddcSAtari911 12421d05cddcSAtari911 /** 12431d05cddcSAtari911 * Check if two events overlap in time 12441d05cddcSAtari911 */ 12451d05cddcSAtari911 private function eventsOverlap($evt1, $evt2) { 12461d05cddcSAtari911 if (empty($evt1['time']) || empty($evt2['time'])) { 12471d05cddcSAtari911 return false; // All-day events don't conflict 12481d05cddcSAtari911 } 12491d05cddcSAtari911 12501d05cddcSAtari911 $start1 = $evt1['time']; 12511d05cddcSAtari911 $end1 = isset($evt1['endTime']) && !empty($evt1['endTime']) ? $evt1['endTime'] : $evt1['time']; 12521d05cddcSAtari911 12531d05cddcSAtari911 $start2 = $evt2['time']; 12541d05cddcSAtari911 $end2 = isset($evt2['endTime']) && !empty($evt2['endTime']) ? $evt2['endTime'] : $evt2['time']; 12551d05cddcSAtari911 12561d05cddcSAtari911 // Convert to minutes for easier comparison 12571d05cddcSAtari911 $start1Mins = $this->timeToMinutes($start1); 12581d05cddcSAtari911 $end1Mins = $this->timeToMinutes($end1); 12591d05cddcSAtari911 $start2Mins = $this->timeToMinutes($start2); 12601d05cddcSAtari911 $end2Mins = $this->timeToMinutes($end2); 12611d05cddcSAtari911 12621d05cddcSAtari911 // Check for overlap: start1 < end2 AND start2 < end1 12631d05cddcSAtari911 return $start1Mins < $end2Mins && $start2Mins < $end1Mins; 12641d05cddcSAtari911 } 12651d05cddcSAtari911 12661d05cddcSAtari911 /** 12671d05cddcSAtari911 * Convert HH:MM time to minutes since midnight 12681d05cddcSAtari911 */ 12691d05cddcSAtari911 private function timeToMinutes($timeStr) { 12701d05cddcSAtari911 $parts = explode(':', $timeStr); 12711d05cddcSAtari911 if (count($parts) !== 2) return 0; 12721d05cddcSAtari911 12731d05cddcSAtari911 return (int)$parts[0] * 60 + (int)$parts[1]; 12741d05cddcSAtari911 } 12751d05cddcSAtari911 127619378907SAtari911 private function renderEventPanelOnly($data) { 127719378907SAtari911 $year = (int)$data['year']; 127819378907SAtari911 $month = (int)$data['month']; 127919378907SAtari911 $namespace = $data['namespace']; 12802866e827SAtari911 $exclude = isset($data['exclude']) ? $data['exclude'] : ''; 12812866e827SAtari911 $excludeList = $this->parseExcludeList($exclude); 128287ac9bf3SAtari911 $height = isset($data['height']) ? $data['height'] : '400px'; 128387ac9bf3SAtari911 128487ac9bf3SAtari911 // Validate height format (must be px, em, rem, vh, or %) 128587ac9bf3SAtari911 if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) { 128687ac9bf3SAtari911 $height = '400px'; // Default fallback 128787ac9bf3SAtari911 } 128819378907SAtari911 12890c3b6e81SAtari911 // Get theme - prefer inline theme= parameter, fall back to admin default 12900c3b6e81SAtari911 $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme(); $themeStyles = $this->getSidebarThemeStyles($theme); 12919ccd446eSAtari911 1292e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 1293e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 1294e3a9f44cSAtari911 1295e3a9f44cSAtari911 if ($isMultiNamespace) { 12962866e827SAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList); 1297e3a9f44cSAtari911 } else { 129819378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 1299e3a9f44cSAtari911 } 130019378907SAtari911 $calId = 'panel_' . md5(serialize($data) . microtime()); 130119378907SAtari911 130219378907SAtari911 $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); 130319378907SAtari911 130419378907SAtari911 $prevMonth = $month - 1; 130519378907SAtari911 $prevYear = $year; 130619378907SAtari911 if ($prevMonth < 1) { 130719378907SAtari911 $prevMonth = 12; 130819378907SAtari911 $prevYear--; 130919378907SAtari911 } 131019378907SAtari911 131119378907SAtari911 $nextMonth = $month + 1; 131219378907SAtari911 $nextYear = $year; 131319378907SAtari911 if ($nextMonth > 12) { 131419378907SAtari911 $nextMonth = 1; 131519378907SAtari911 $nextYear++; 131619378907SAtari911 } 131719378907SAtari911 13189ccd446eSAtari911 // Determine button text color based on theme 13199ccd446eSAtari911 $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg']; 13209ccd446eSAtari911 132196df7d3eSAtari911 // Get important namespaces from config for highlighting 13222866e827SAtari911 $configFile = $this->syncConfigPath(); 132396df7d3eSAtari911 $importantNsList = ['important']; // default 132496df7d3eSAtari911 if (file_exists($configFile)) { 132596df7d3eSAtari911 $config = include $configFile; 132696df7d3eSAtari911 if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) { 132796df7d3eSAtari911 $importantNsList = array_map('trim', explode(',', $config['important_namespaces'])); 132896df7d3eSAtari911 } 132996df7d3eSAtari911 } 133096df7d3eSAtari911 13312866e827SAtari911 $html = '<div class="event-panel-standalone" id="' . $calId . '" data-height="' . htmlspecialchars($height) . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '" data-exclude="' . htmlspecialchars($exclude) . '" data-theme="' . $theme . '" data-theme-styles="' . htmlspecialchars(json_encode($themeStyles)) . '" data-important-namespaces="' . htmlspecialchars(json_encode($importantNsList)) . '">'; 13329ccd446eSAtari911 13339ccd446eSAtari911 // Inject CSS variables for this panel instance - same as main calendar 13349ccd446eSAtari911 $html .= '<style> 13359ccd446eSAtari911 #' . $calId . ' { 13369ccd446eSAtari911 --background-site: ' . $themeStyles['bg'] . '; 13379ccd446eSAtari911 --background-alt: ' . $themeStyles['cell_bg'] . '; 13389ccd446eSAtari911 --background-header: ' . $themeStyles['header_bg'] . '; 13399ccd446eSAtari911 --text-primary: ' . $themeStyles['text_primary'] . '; 13409ccd446eSAtari911 --text-dim: ' . $themeStyles['text_dim'] . '; 13419ccd446eSAtari911 --text-bright: ' . $themeStyles['text_bright'] . '; 13429ccd446eSAtari911 --border-color: ' . $themeStyles['grid_border'] . '; 13439ccd446eSAtari911 --border-main: ' . $themeStyles['border'] . '; 13449ccd446eSAtari911 --cell-bg: ' . $themeStyles['cell_bg'] . '; 13459ccd446eSAtari911 --cell-today-bg: ' . $themeStyles['cell_today_bg'] . '; 13469ccd446eSAtari911 --shadow-color: ' . $themeStyles['shadow'] . '; 13479ccd446eSAtari911 --header-border: ' . $themeStyles['header_border'] . '; 13489ccd446eSAtari911 --header-shadow: ' . $themeStyles['header_shadow'] . '; 13499ccd446eSAtari911 --grid-bg: ' . $themeStyles['grid_bg'] . '; 13509ccd446eSAtari911 --btn-text: ' . $btnTextColor . '; 13517e8ea635SAtari911 --pastdue-color: ' . $themeStyles['pastdue_color'] . '; 13527e8ea635SAtari911 --pastdue-bg: ' . $themeStyles['pastdue_bg'] . '; 13537e8ea635SAtari911 --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . '; 13547e8ea635SAtari911 --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . '; 13557e8ea635SAtari911 --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . '; 13567e8ea635SAtari911 --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . '; 13577e8ea635SAtari911 --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . '; 13589ccd446eSAtari911 } 13599ccd446eSAtari911 #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; } 13609ccd446eSAtari911 </style>'; 136119378907SAtari911 13621d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 13631d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 13641d05cddcSAtari911 13651d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 13661d05cddcSAtari911 $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>'; 13671d05cddcSAtari911 13681d05cddcSAtari911 // Compact two-row header designed for ~500px width 13691d05cddcSAtari911 $html .= '<div class="panel-header-compact">'; 13701d05cddcSAtari911 13711d05cddcSAtari911 // Row 1: Navigation and title 13721d05cddcSAtari911 $html .= '<div class="panel-header-row-1">'; 13731d05cddcSAtari911 $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 13741d05cddcSAtari911 13751d05cddcSAtari911 // Compact month name (e.g. "Feb 2026" instead of "February 2026 Events") 13761d05cddcSAtari911 $shortMonthName = date('M Y', mktime(0, 0, 0, $month, 1, $year)); 13771d05cddcSAtari911 $html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $shortMonthName . '</h3>'; 13781d05cddcSAtari911 13791d05cddcSAtari911 $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 13801d05cddcSAtari911 13811d05cddcSAtari911 // Namespace badge (if applicable) 138287ac9bf3SAtari911 if ($namespace) { 1383e3a9f44cSAtari911 if ($isMultiNamespace) { 1384e3a9f44cSAtari911 if (strpos($namespace, '*') !== false) { 13857e8ea635SAtari911 $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>'; 1386e3a9f44cSAtari911 } else { 1387e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespace)); 13881d05cddcSAtari911 $nsCount = count($namespaceList); 13897e8ea635SAtari911 $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>'; 1390e3a9f44cSAtari911 } 1391e3a9f44cSAtari911 } else { 13921d05cddcSAtari911 $isFiltering = ($namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false); 13931d05cddcSAtari911 if ($isFiltering) { 13947e8ea635SAtari911 $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>'; 13951d05cddcSAtari911 } else { 13967e8ea635SAtari911 $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>'; 139787ac9bf3SAtari911 } 1398e3a9f44cSAtari911 } 13991d05cddcSAtari911 } 14001d05cddcSAtari911 1401da206178SAtari911 $html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>'; 140219378907SAtari911 $html .= '</div>'; 140319378907SAtari911 14041d05cddcSAtari911 // Row 2: Search and add button 140564a96c92SAtari911 $searchDefault = $this->getSearchDefault(); 140664a96c92SAtari911 $searchAllClass = $searchDefault === 'all' ? ' all-dates' : ''; 140764a96c92SAtari911 $searchIcon = $searchDefault === 'all' ? '' : ''; 140864a96c92SAtari911 $searchTitle = $searchDefault === 'all' ? 'Searching all dates' : 'Search this month only'; 140964a96c92SAtari911 $searchPlaceholder = $searchDefault === 'all' ? 'Search all dates...' : 'Search this month...'; 14101d05cddcSAtari911 $html .= '<div class="panel-header-row-2">'; 14111d05cddcSAtari911 $html .= '<div class="panel-search-box">'; 141264a96c92SAtari911 $html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placeholder="' . htmlspecialchars($searchPlaceholder) . '" oninput="filterEvents(\'' . $calId . '\', this.value)">'; 14131d05cddcSAtari911 $html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>'; 141464a96c92SAtari911 $html .= '<button class="panel-search-mode' . $searchAllClass . '" id="search-mode-' . $calId . '" onclick="toggleSearchMode(\'' . $calId . '\', \'' . $namespace . '\')" title="' . htmlspecialchars($searchTitle) . '">' . $searchIcon . '</button>'; 14151d05cddcSAtari911 $html .= '</div>'; 14161d05cddcSAtari911 $html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>'; 14171d05cddcSAtari911 $html .= '</div>'; 14181d05cddcSAtari911 141919378907SAtari911 $html .= '</div>'; 142019378907SAtari911 142187ac9bf3SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">'; 142219378907SAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace); 142319378907SAtari911 $html .= '</div>'; 142419378907SAtari911 14250c3b6e81SAtari911 $html .= $this->renderEventDialog($calId, $namespace, $theme); 142619378907SAtari911 142787ac9bf3SAtari911 // Month/Year picker for event panel 14289ccd446eSAtari911 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles); 142987ac9bf3SAtari911 143019378907SAtari911 $html .= '</div>'; 143119378907SAtari911 143219378907SAtari911 return $html; 143319378907SAtari911 } 143419378907SAtari911 143519378907SAtari911 private function renderStandaloneEventList($data) { 143619378907SAtari911 $namespace = $data['namespace']; 14371d05cddcSAtari911 // If no namespace specified, show all namespaces 14381d05cddcSAtari911 if (empty($namespace)) { 14391d05cddcSAtari911 $namespace = '*'; 14401d05cddcSAtari911 } 14412866e827SAtari911 $exclude = isset($data['exclude']) ? $data['exclude'] : ''; 14422866e827SAtari911 $excludeList = $this->parseExcludeList($exclude); 144319378907SAtari911 $daterange = $data['daterange']; 144419378907SAtari911 $date = $data['date']; 1445e3a9f44cSAtari911 $range = isset($data['range']) ? strtolower($data['range']) : ''; 144687ac9bf3SAtari911 $today = isset($data['today']) ? true : false; 1447e3a9f44cSAtari911 $sidebar = isset($data['sidebar']) ? true : false; 14481d05cddcSAtari911 $showchecked = isset($data['showchecked']) ? true : false; // New parameter 14491d05cddcSAtari911 $noheader = isset($data['noheader']) ? true : false; // New parameter to hide header 145019378907SAtari911 1451e3a9f44cSAtari911 // Handle "range" parameter - day, week, or month 1452e3a9f44cSAtari911 if ($range === 'day') { 14531d05cddcSAtari911 $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks 145487ac9bf3SAtari911 $endDate = date('Y-m-d'); 1455da206178SAtari911 $headerText = 'Today'; 1456e3a9f44cSAtari911 } elseif ($range === 'week') { 14571d05cddcSAtari911 $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks 14581d05cddcSAtari911 $endDateTime = new DateTime(); 1459e3a9f44cSAtari911 $endDateTime->modify('+7 days'); 1460e3a9f44cSAtari911 $endDate = $endDateTime->format('Y-m-d'); 1461da206178SAtari911 $headerText = 'This Week'; 1462e3a9f44cSAtari911 } elseif ($range === 'month') { 14631d05cddcSAtari911 $startDate = date('Y-m-01', strtotime('-1 month')); // Include previous month for past due tasks 1464e3a9f44cSAtari911 $endDate = date('Y-m-t'); // Last of current month 14651d05cddcSAtari911 $dt = new DateTime(); 1466e3a9f44cSAtari911 $headerText = $dt->format('F Y'); 1467e3a9f44cSAtari911 } elseif ($sidebar) { 14681d05cddcSAtari911 // NEW: Sidebar widget - load current week's events 14699ccd446eSAtari911 $weekStartDay = $this->getWeekStartDay(); // Get saved preference 14709ccd446eSAtari911 14719ccd446eSAtari911 if ($weekStartDay === 'monday') { 14729ccd446eSAtari911 // Monday start 14731d05cddcSAtari911 $weekStart = date('Y-m-d', strtotime('monday this week')); 14741d05cddcSAtari911 $weekEnd = date('Y-m-d', strtotime('sunday this week')); 14759ccd446eSAtari911 } else { 14769ccd446eSAtari911 // Sunday start (default - US/Canada standard) 14779ccd446eSAtari911 $today = date('w'); // 0 (Sun) to 6 (Sat) 14789ccd446eSAtari911 if ($today == 0) { 14799ccd446eSAtari911 // Today is Sunday 14809ccd446eSAtari911 $weekStart = date('Y-m-d'); 14819ccd446eSAtari911 } else { 14829ccd446eSAtari911 // Monday-Saturday: go back to last Sunday 14839ccd446eSAtari911 $weekStart = date('Y-m-d', strtotime('-' . $today . ' days')); 14849ccd446eSAtari911 } 14859ccd446eSAtari911 $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days')); 14869ccd446eSAtari911 } 14871d05cddcSAtari911 14889ccd446eSAtari911 // Load events for the entire week PLUS tomorrow (if tomorrow is outside week) 14899ccd446eSAtari911 // PLUS next 2 weeks for Important events 14901d05cddcSAtari911 $start = new DateTime($weekStart); 14911d05cddcSAtari911 $end = new DateTime($weekEnd); 14929ccd446eSAtari911 14939ccd446eSAtari911 // Check if we need to extend to include tomorrow 14949ccd446eSAtari911 $tomorrowDate = date('Y-m-d', strtotime('+1 day')); 14959ccd446eSAtari911 if ($tomorrowDate > $weekEnd) { 14969ccd446eSAtari911 // Tomorrow is outside the week, extend end date to include it 14979ccd446eSAtari911 $end = new DateTime($tomorrowDate); 14989ccd446eSAtari911 } 14999ccd446eSAtari911 15009ccd446eSAtari911 // Extend 2 weeks into the future for Important events 15019ccd446eSAtari911 $twoWeeksOut = date('Y-m-d', strtotime($weekEnd . ' +14 days')); 15029ccd446eSAtari911 $end = new DateTime($twoWeeksOut); 15039ccd446eSAtari911 15041d05cddcSAtari911 $end->modify('+1 day'); // DatePeriod excludes end date 15051d05cddcSAtari911 $interval = new DateInterval('P1D'); 15061d05cddcSAtari911 $period = new DatePeriod($start, $interval, $end); 15071d05cddcSAtari911 15081d05cddcSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 15091d05cddcSAtari911 $allEvents = []; 15101d05cddcSAtari911 $loadedMonths = []; 15111d05cddcSAtari911 15121d05cddcSAtari911 foreach ($period as $dt) { 15131d05cddcSAtari911 $year = (int)$dt->format('Y'); 15141d05cddcSAtari911 $month = (int)$dt->format('n'); 15151d05cddcSAtari911 $dateKey = $dt->format('Y-m-d'); 15161d05cddcSAtari911 15171d05cddcSAtari911 $monthKey = $year . '-' . $month . '-' . $namespace; 15181d05cddcSAtari911 15191d05cddcSAtari911 if (!isset($loadedMonths[$monthKey])) { 15201d05cddcSAtari911 if ($isMultiNamespace) { 15212866e827SAtari911 $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList); 15221d05cddcSAtari911 } else { 15231d05cddcSAtari911 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month); 15241d05cddcSAtari911 } 15251d05cddcSAtari911 } 15261d05cddcSAtari911 15271d05cddcSAtari911 $monthEvents = $loadedMonths[$monthKey]; 15281d05cddcSAtari911 15291d05cddcSAtari911 if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) { 15301d05cddcSAtari911 $allEvents[$dateKey] = $monthEvents[$dateKey]; 15311d05cddcSAtari911 } 15321d05cddcSAtari911 } 15331d05cddcSAtari911 15341d05cddcSAtari911 // Apply time conflict detection 15351d05cddcSAtari911 $allEvents = $this->checkTimeConflicts($allEvents); 15361d05cddcSAtari911 15371d05cddcSAtari911 $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8); 15381d05cddcSAtari911 15391d05cddcSAtari911 // Render sidebar widget and return immediately 15400c3b6e81SAtari911 $themeOverride = !empty($data['theme']) ? $data['theme'] : null; 15410c3b6e81SAtari911 return $this->renderSidebarWidget($allEvents, $namespace, $calId, $themeOverride); 1542e3a9f44cSAtari911 } elseif ($today) { 1543e3a9f44cSAtari911 $startDate = date('Y-m-d'); 1544e3a9f44cSAtari911 $endDate = date('Y-m-d'); 1545da206178SAtari911 $headerText = 'Today'; 154687ac9bf3SAtari911 } elseif ($daterange) { 154719378907SAtari911 list($startDate, $endDate) = explode(':', $daterange); 1548e3a9f44cSAtari911 $start = new DateTime($startDate); 1549e3a9f44cSAtari911 $end = new DateTime($endDate); 1550e3a9f44cSAtari911 $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y'); 155119378907SAtari911 } elseif ($date) { 155219378907SAtari911 $startDate = $date; 155319378907SAtari911 $endDate = $date; 1554e3a9f44cSAtari911 $dt = new DateTime($date); 1555e3a9f44cSAtari911 $headerText = $dt->format('l, F j, Y'); 155619378907SAtari911 } else { 155719378907SAtari911 $startDate = date('Y-m-01'); 155819378907SAtari911 $endDate = date('Y-m-t'); 1559e3a9f44cSAtari911 $dt = new DateTime($startDate); 1560e3a9f44cSAtari911 $headerText = $dt->format('F Y'); 156119378907SAtari911 } 156219378907SAtari911 1563e3a9f44cSAtari911 // Load all events in date range 156419378907SAtari911 $allEvents = array(); 156519378907SAtari911 $start = new DateTime($startDate); 156619378907SAtari911 $end = new DateTime($endDate); 156719378907SAtari911 $end->modify('+1 day'); 156819378907SAtari911 156919378907SAtari911 $interval = new DateInterval('P1D'); 157019378907SAtari911 $period = new DatePeriod($start, $interval, $end); 157119378907SAtari911 1572e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 1573e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 1574e3a9f44cSAtari911 157519378907SAtari911 static $loadedMonths = array(); 157619378907SAtari911 157719378907SAtari911 foreach ($period as $dt) { 157819378907SAtari911 $year = (int)$dt->format('Y'); 157919378907SAtari911 $month = (int)$dt->format('n'); 158019378907SAtari911 $dateKey = $dt->format('Y-m-d'); 158119378907SAtari911 1582e3a9f44cSAtari911 $monthKey = $year . '-' . $month . '-' . $namespace; 158319378907SAtari911 158419378907SAtari911 if (!isset($loadedMonths[$monthKey])) { 1585e3a9f44cSAtari911 if ($isMultiNamespace) { 15862866e827SAtari911 $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList); 1587e3a9f44cSAtari911 } else { 158819378907SAtari911 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month); 158919378907SAtari911 } 1590e3a9f44cSAtari911 } 159119378907SAtari911 159219378907SAtari911 $monthEvents = $loadedMonths[$monthKey]; 159319378907SAtari911 159419378907SAtari911 if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) { 159519378907SAtari911 $allEvents[$dateKey] = $monthEvents[$dateKey]; 159619378907SAtari911 } 159719378907SAtari911 } 159819378907SAtari911 15991d05cddcSAtari911 // Sort events by date (already sorted by dateKey), then by time within each day 16001d05cddcSAtari911 foreach ($allEvents as $dateKey => &$dayEvents) { 16011d05cddcSAtari911 usort($dayEvents, function($a, $b) { 16021d05cddcSAtari911 $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null; 16031d05cddcSAtari911 $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null; 16041d05cddcSAtari911 16051d05cddcSAtari911 // All-day events (no time) go to the TOP 16061d05cddcSAtari911 if ($timeA === null && $timeB !== null) return -1; // A before B 16071d05cddcSAtari911 if ($timeA !== null && $timeB === null) return 1; // A after B 16081d05cddcSAtari911 if ($timeA === null && $timeB === null) return 0; // Both all-day, equal 16091d05cddcSAtari911 16101d05cddcSAtari911 // Both have times, sort chronologically 16111d05cddcSAtari911 return strcmp($timeA, $timeB); 16121d05cddcSAtari911 }); 16131d05cddcSAtari911 } 16141d05cddcSAtari911 unset($dayEvents); // Break reference 16151d05cddcSAtari911 1616e3a9f44cSAtari911 // Simple 2-line display widget 16171d05cddcSAtari911 $calId = 'eventlist_' . uniqid(); 16187e8ea635SAtari911 $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme(); 16197e8ea635SAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 16207e8ea635SAtari911 $isDark = in_array($theme, ['matrix', 'purple', 'pink']); 16217e8ea635SAtari911 $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg']; 16227e8ea635SAtari911 16237e8ea635SAtari911 // Theme class for CSS targeting 16247e8ea635SAtari911 $themeClass = 'eventlist-theme-' . $theme; 16257e8ea635SAtari911 16267e8ea635SAtari911 // Container styling - dark themes get border + glow, light themes get subtle border 16277e8ea635SAtari911 $containerStyle = 'background:' . $themeStyles['bg'] . ' !important;'; 16287e8ea635SAtari911 if ($isDark) { 16297e8ea635SAtari911 $containerStyle .= ' border:2px solid ' . $themeStyles['border'] . ';'; 16307e8ea635SAtari911 $containerStyle .= ' border-radius:4px;'; 16317e8ea635SAtari911 $containerStyle .= ' box-shadow:0 0 10px ' . $themeStyles['shadow'] . ';'; 16327e8ea635SAtari911 } else { 16337e8ea635SAtari911 $containerStyle .= ' border:1px solid ' . $themeStyles['grid_border'] . ';'; 16347e8ea635SAtari911 $containerStyle .= ' border-radius:4px;'; 16357e8ea635SAtari911 } 16367e8ea635SAtari911 16372866e827SAtari911 $html = '<div class="eventlist-simple ' . $themeClass . '" id="' . $calId . '" style="' . $containerStyle . '" data-exclude="' . htmlspecialchars($exclude) . '">'; 16387e8ea635SAtari911 16397e8ea635SAtari911 // Inject CSS variables for this eventlist instance 16407e8ea635SAtari911 $html .= '<style> 16417e8ea635SAtari911 #' . $calId . ' { 16427e8ea635SAtari911 --background-site: ' . $themeStyles['bg'] . '; 16437e8ea635SAtari911 --background-alt: ' . $themeStyles['cell_bg'] . '; 16447e8ea635SAtari911 --text-primary: ' . $themeStyles['text_primary'] . '; 16457e8ea635SAtari911 --text-dim: ' . $themeStyles['text_dim'] . '; 16467e8ea635SAtari911 --text-bright: ' . $themeStyles['text_bright'] . '; 16477e8ea635SAtari911 --border-color: ' . $themeStyles['grid_border'] . '; 16487e8ea635SAtari911 --border-main: ' . $themeStyles['border'] . '; 16497e8ea635SAtari911 --cell-bg: ' . $themeStyles['cell_bg'] . '; 16507e8ea635SAtari911 --cell-today-bg: ' . $themeStyles['cell_today_bg'] . '; 16517e8ea635SAtari911 --shadow-color: ' . $themeStyles['shadow'] . '; 16527e8ea635SAtari911 --grid-bg: ' . $themeStyles['grid_bg'] . '; 16537e8ea635SAtari911 --btn-text: ' . $btnTextColor . '; 16547e8ea635SAtari911 --pastdue-color: ' . $themeStyles['pastdue_color'] . '; 16557e8ea635SAtari911 --pastdue-bg: ' . $themeStyles['pastdue_bg'] . '; 16567e8ea635SAtari911 --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . '; 16577e8ea635SAtari911 --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . '; 16587e8ea635SAtari911 --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . '; 16597e8ea635SAtari911 --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . '; 16607e8ea635SAtari911 --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . '; 16617e8ea635SAtari911 } 16627e8ea635SAtari911 </style>'; 16631d05cddcSAtari911 16641d05cddcSAtari911 // Load calendar JavaScript manually (not through DokuWiki concatenation) 16651d05cddcSAtari911 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>'; 16661d05cddcSAtari911 16671d05cddcSAtari911 // Initialize DOKU_BASE for JavaScript 16681d05cddcSAtari911 $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>'; 16691d05cddcSAtari911 16701d05cddcSAtari911 // Add compact header with date and clock for "today" mode (unless noheader is set) 16711d05cddcSAtari911 if ($today && !empty($allEvents) && !$noheader) { 16721d05cddcSAtari911 $todayDate = new DateTime(); 16731d05cddcSAtari911 $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026" 16741d05cddcSAtari911 $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM" 16751d05cddcSAtari911 16761d05cddcSAtari911 $html .= '<div class="eventlist-today-header">'; 16771d05cddcSAtari911 $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>'; 16781d05cddcSAtari911 $html .= '<div class="eventlist-bottom-info">'; 16791d05cddcSAtari911 $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">️</span> <span id="weather-temp-' . $calId . '">--°</span></span>'; 16801d05cddcSAtari911 $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>'; 16811d05cddcSAtari911 $html .= '</div>'; 16821d05cddcSAtari911 16831d05cddcSAtari911 $html .= '</div>'; 16841d05cddcSAtari911 16851d05cddcSAtari911 // Add JavaScript to update clock and weather 16861d05cddcSAtari911 $html .= '<script> 16871d05cddcSAtari911(function() { 16881d05cddcSAtari911 // Update clock every second 16891d05cddcSAtari911 function updateClock() { 16901d05cddcSAtari911 const now = new Date(); 16911d05cddcSAtari911 let hours = now.getHours(); 16921d05cddcSAtari911 const minutes = String(now.getMinutes()).padStart(2, "0"); 16931d05cddcSAtari911 const seconds = String(now.getSeconds()).padStart(2, "0"); 16941d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 16951d05cddcSAtari911 hours = hours % 12 || 12; 16961d05cddcSAtari911 const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm; 16971d05cddcSAtari911 const clockEl = document.getElementById("clock-' . $calId . '"); 16981d05cddcSAtari911 if (clockEl) clockEl.textContent = timeStr; 16991d05cddcSAtari911 } 17001d05cddcSAtari911 setInterval(updateClock, 1000); 17011d05cddcSAtari911 170296df7d3eSAtari911 // Fetch weather - uses default location, click weather to get local 170396df7d3eSAtari911 var userLocationGranted = false; 170496df7d3eSAtari911 var userLat = 38.5816; // Sacramento default 170596df7d3eSAtari911 var userLon = -121.4944; 17061d05cddcSAtari911 170796df7d3eSAtari911 function fetchWeatherData(lat, lon) { 170896df7d3eSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "¤t_weather=true&temperature_unit=fahrenheit") 17091d05cddcSAtari911 .then(response => response.json()) 17101d05cddcSAtari911 .then(data => { 17111d05cddcSAtari911 if (data.current_weather) { 17121d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 17131d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 17141d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 17151d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 17161d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 17171d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 17181d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 17191d05cddcSAtari911 } 17201d05cddcSAtari911 }) 17211d05cddcSAtari911 .catch(error => { 17221d05cddcSAtari911 console.log("Weather fetch error:", error); 17231d05cddcSAtari911 }); 172496df7d3eSAtari911 } 172596df7d3eSAtari911 172696df7d3eSAtari911 function updateWeather() { 172796df7d3eSAtari911 fetchWeatherData(userLat, userLon); 172896df7d3eSAtari911 } 172996df7d3eSAtari911 173096df7d3eSAtari911 // Allow user to click weather to get local weather (requires user gesture) 173196df7d3eSAtari911 function requestLocalWeather() { 173296df7d3eSAtari911 if (userLocationGranted) return; // Already have permission 173396df7d3eSAtari911 173496df7d3eSAtari911 if ("geolocation" in navigator) { 173596df7d3eSAtari911 navigator.geolocation.getCurrentPosition(function(position) { 173696df7d3eSAtari911 userLat = position.coords.latitude; 173796df7d3eSAtari911 userLon = position.coords.longitude; 173896df7d3eSAtari911 userLocationGranted = true; 173996df7d3eSAtari911 fetchWeatherData(userLat, userLon); 17401d05cddcSAtari911 }, function(error) { 174196df7d3eSAtari911 console.log("Geolocation denied or unavailable, using default location"); 17421d05cddcSAtari911 }); 17431d05cddcSAtari911 } 17441d05cddcSAtari911 } 17451d05cddcSAtari911 174696df7d3eSAtari911 // Add click handler to weather widget for local weather 174796df7d3eSAtari911 setTimeout(function() { 174896df7d3eSAtari911 var weatherEl = document.querySelector("#weather-icon-' . $calId . '"); 174996df7d3eSAtari911 if (weatherEl) { 175096df7d3eSAtari911 weatherEl.style.cursor = "pointer"; 175196df7d3eSAtari911 weatherEl.title = "Click for local weather"; 175296df7d3eSAtari911 weatherEl.addEventListener("click", requestLocalWeather); 175396df7d3eSAtari911 } 175496df7d3eSAtari911 }, 100); 175596df7d3eSAtari911 17561d05cddcSAtari911 // WMO Weather interpretation codes 17571d05cddcSAtari911 function getWeatherIcon(code) { 17581d05cddcSAtari911 const icons = { 17591d05cddcSAtari911 0: "☀️", // Clear sky 17601d05cddcSAtari911 1: "️", // Mainly clear 17611d05cddcSAtari911 2: "⛅", // Partly cloudy 17621d05cddcSAtari911 3: "☁️", // Overcast 17631d05cddcSAtari911 45: "️", // Fog 17641d05cddcSAtari911 48: "️", // Depositing rime fog 17651d05cddcSAtari911 51: "️", // Light drizzle 17661d05cddcSAtari911 53: "️", // Moderate drizzle 17671d05cddcSAtari911 55: "️", // Dense drizzle 17681d05cddcSAtari911 61: "️", // Slight rain 17691d05cddcSAtari911 63: "️", // Moderate rain 17701d05cddcSAtari911 65: "⛈️", // Heavy rain 17711d05cddcSAtari911 71: "️", // Slight snow 17721d05cddcSAtari911 73: "️", // Moderate snow 17731d05cddcSAtari911 75: "❄️", // Heavy snow 17741d05cddcSAtari911 77: "️", // Snow grains 17751d05cddcSAtari911 80: "️", // Slight rain showers 17761d05cddcSAtari911 81: "️", // Moderate rain showers 17771d05cddcSAtari911 82: "⛈️", // Violent rain showers 17781d05cddcSAtari911 85: "️", // Slight snow showers 17791d05cddcSAtari911 86: "❄️", // Heavy snow showers 17801d05cddcSAtari911 95: "⛈️", // Thunderstorm 17811d05cddcSAtari911 96: "⛈️", // Thunderstorm with slight hail 17821d05cddcSAtari911 99: "⛈️" // Thunderstorm with heavy hail 17831d05cddcSAtari911 }; 17841d05cddcSAtari911 return icons[code] || "️"; 17851d05cddcSAtari911 } 17861d05cddcSAtari911 17871d05cddcSAtari911 // Update weather immediately and every 10 minutes 17881d05cddcSAtari911 updateWeather(); 17891d05cddcSAtari911 setInterval(updateWeather, 600000); 17901d05cddcSAtari911})(); 17911d05cddcSAtari911</script>'; 17921d05cddcSAtari911 } 179319378907SAtari911 179419378907SAtari911 if (empty($allEvents)) { 1795e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-empty">'; 1796e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText); 1797e3a9f44cSAtari911 if ($namespace) { 1798e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>'; 179987ac9bf3SAtari911 } 1800e3a9f44cSAtari911 $html .= '</div>'; 1801e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-body">No events</div>'; 1802e3a9f44cSAtari911 $html .= '</div>'; 1803e3a9f44cSAtari911 } else { 1804e3a9f44cSAtari911 // Calculate today and tomorrow's dates for highlighting 18051d05cddcSAtari911 $todayStr = date('Y-m-d'); 1806e3a9f44cSAtari911 $tomorrow = date('Y-m-d', strtotime('+1 day')); 1807e3a9f44cSAtari911 1808e3a9f44cSAtari911 foreach ($allEvents as $dateKey => $dayEvents) { 1809e3a9f44cSAtari911 $dateObj = new DateTime($dateKey); 1810e3a9f44cSAtari911 $displayDate = $dateObj->format('D, M j'); 1811e3a9f44cSAtari911 18121d05cddcSAtari911 // Check if this date is today or tomorrow or past 1813e3a9f44cSAtari911 // Enable highlighting for sidebar mode AND range modes (day, week, month) 1814e3a9f44cSAtari911 $enableHighlighting = $sidebar || !empty($range); 18151d05cddcSAtari911 $isToday = $enableHighlighting && ($dateKey === $todayStr); 1816e3a9f44cSAtari911 $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow); 18171d05cddcSAtari911 $isPast = $dateKey < $todayStr; 181819378907SAtari911 181919378907SAtari911 foreach ($dayEvents as $event) { 18201d05cddcSAtari911 // Check if this is a task and if it's completed 18211d05cddcSAtari911 $isTask = !empty($event['isTask']); 18221d05cddcSAtari911 $completed = !empty($event['completed']); 18231d05cddcSAtari911 18241d05cddcSAtari911 // ALWAYS skip completed tasks UNLESS showchecked is explicitly set 18251d05cddcSAtari911 if (!$showchecked && $isTask && $completed) { 1826e3a9f44cSAtari911 continue; 1827e3a9f44cSAtari911 } 182819378907SAtari911 18291d05cddcSAtari911 // Skip past events that are NOT tasks (only show past due tasks from the past) 18301d05cddcSAtari911 if ($isPast && !$isTask) { 18311d05cddcSAtari911 continue; 18321d05cddcSAtari911 } 18331d05cddcSAtari911 18341d05cddcSAtari911 // Determine if task is past due (past date, is task, not completed) 18351d05cddcSAtari911 $isPastDue = $isPast && $isTask && !$completed; 18361d05cddcSAtari911 1837e3a9f44cSAtari911 // Line 1: Header (Title, Time, Date, Namespace) 1838e3a9f44cSAtari911 $todayClass = $isToday ? ' eventlist-simple-today' : ''; 1839e3a9f44cSAtari911 $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : ''; 18401d05cddcSAtari911 $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : ''; 18411d05cddcSAtari911 $html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . $pastDueClass . '">'; 1842e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-header">'; 1843e3a9f44cSAtari911 1844e3a9f44cSAtari911 // Title 1845e3a9f44cSAtari911 $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>'; 1846e3a9f44cSAtari911 1847e3a9f44cSAtari911 // Time (12-hour format) 1848e3a9f44cSAtari911 if (!empty($event['time'])) { 1849e3a9f44cSAtari911 $timeParts = explode(':', $event['time']); 185087ac9bf3SAtari911 if (count($timeParts) === 2) { 185187ac9bf3SAtari911 $hour = (int)$timeParts[0]; 185287ac9bf3SAtari911 $minute = $timeParts[1]; 185387ac9bf3SAtari911 $ampm = $hour >= 12 ? 'PM' : 'AM'; 1854e3a9f44cSAtari911 $hour = $hour % 12 ?: 12; 185587ac9bf3SAtari911 $displayTime = $hour . ':' . $minute . ' ' . $ampm; 1856e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>'; 185719378907SAtari911 } 185887ac9bf3SAtari911 } 185987ac9bf3SAtari911 1860e3a9f44cSAtari911 // Date 1861e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>'; 1862e3a9f44cSAtari911 18631d05cddcSAtari911 // Badge: PAST DUE, TODAY, or nothing 18641d05cddcSAtari911 if ($isPastDue) { 18657e8ea635SAtari911 $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>'; 18661d05cddcSAtari911 } elseif ($isToday) { 18677e8ea635SAtari911 $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>'; 186887ac9bf3SAtari911 } 1869e3a9f44cSAtari911 1870e3a9f44cSAtari911 // Namespace badge (show individual event's namespace) 1871e3a9f44cSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 1872e3a9f44cSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 1873e3a9f44cSAtari911 $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading 187419378907SAtari911 } 1875e3a9f44cSAtari911 if ($eventNamespace) { 1876e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</span>'; 1877e3a9f44cSAtari911 } 1878e3a9f44cSAtari911 1879e3a9f44cSAtari911 $html .= '</div>'; // header 1880e3a9f44cSAtari911 1881e3a9f44cSAtari911 // Line 2: Body (Description only) - only show if description exists 1882e3a9f44cSAtari911 if (!empty($event['description'])) { 1883e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) . '</div>'; 1884e3a9f44cSAtari911 } 1885e3a9f44cSAtari911 1886e3a9f44cSAtari911 $html .= '</div>'; // item 188719378907SAtari911 } 188819378907SAtari911 } 188987ac9bf3SAtari911 } 189019378907SAtari911 1891e3a9f44cSAtari911 $html .= '</div>'; // eventlist-simple 189219378907SAtari911 189319378907SAtari911 return $html; 189419378907SAtari911 } 189519378907SAtari911 18960c3b6e81SAtari911 private function renderEventDialog($calId, $namespace, $theme = null) { 18979ccd446eSAtari911 // Get theme for dialog 18980c3b6e81SAtari911 if ($theme === null) { 18999ccd446eSAtari911 $theme = $this->getSidebarTheme(); 19000c3b6e81SAtari911 } 19019ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 19029ccd446eSAtari911 190319378907SAtari911 $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">'; 190419378907SAtari911 $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>'; 190519378907SAtari911 19069ccd446eSAtari911 // Draggable dialog with theme 190719378907SAtari911 $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">'; 190819378907SAtari911 190919378907SAtari911 // Header with drag handle and close button 191019378907SAtari911 $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">'; 1911da206178SAtari911 $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>'; 191219378907SAtari911 $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>'; 191319378907SAtari911 $html .= '</div>'; 191419378907SAtari911 191519378907SAtari911 // Form content 191619378907SAtari911 $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">'; 191719378907SAtari911 191819378907SAtari911 // Hidden ID field 191919378907SAtari911 $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">'; 192019378907SAtari911 19211d05cddcSAtari911 // 1. TITLE 19221d05cddcSAtari911 $html .= '<div class="form-field">'; 1923da206178SAtari911 $html .= '<label class="field-label"> Title</label>'; 1924da206178SAtari911 $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek input-compact" placeholder="Event or task title...">'; 192519378907SAtari911 $html .= '</div>'; 192619378907SAtari911 19271d05cddcSAtari911 // 1.5 NAMESPACE SELECTOR (Searchable with fuzzy matching) 19281d05cddcSAtari911 $html .= '<div class="form-field">'; 1929da206178SAtari911 $html .= '<label class="field-label"> Namespace</label>'; 19301d05cddcSAtari911 19311d05cddcSAtari911 // Hidden field to store actual selected namespace 19321d05cddcSAtari911 $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">'; 19331d05cddcSAtari911 19341d05cddcSAtari911 // Searchable input 19351d05cddcSAtari911 $html .= '<div class="namespace-search-wrapper">'; 1936da206178SAtari911 $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">'; 19371d05cddcSAtari911 $html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="display:none;"></div>'; 19381d05cddcSAtari911 $html .= '</div>'; 19391d05cddcSAtari911 19401d05cddcSAtari911 // Store namespaces as JSON for JavaScript 19411d05cddcSAtari911 $allNamespaces = $this->getAllNamespaces(); 19421d05cddcSAtari911 $html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($allNamespaces) . '</script>'; 19431d05cddcSAtari911 19441d05cddcSAtari911 $html .= '</div>'; 19451d05cddcSAtari911 19461d05cddcSAtari911 // 2. DESCRIPTION 19471d05cddcSAtari911 $html .= '<div class="form-field">'; 1948da206178SAtari911 $html .= '<label class="field-label"> Description</label>'; 1949da206178SAtari911 $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="2" class="input-sleek textarea-sleek textarea-compact" placeholder="Optional details..."></textarea>'; 19501d05cddcSAtari911 $html .= '</div>'; 19511d05cddcSAtari911 19521d05cddcSAtari911 // 3. START DATE - END DATE (inline) 195319378907SAtari911 $html .= '<div class="form-row-group">'; 195419378907SAtari911 19551d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 1956da206178SAtari911 $html .= '<label class="field-label-compact"> Start Date</label>'; 1957815440faSAtari911 $html .= '<div class="date-picker-wrapper">'; 1958815440faSAtari911 $html .= '<input type="hidden" id="event-date-' . $calId . '" name="date" required value="">'; 1959815440faSAtari911 $html .= '<button type="button" class="custom-date-picker input-sleek input-compact" id="date-picker-btn-' . $calId . '" data-target="event-date-' . $calId . '" data-type="start">'; 1960815440faSAtari911 $html .= '<span class="date-display">Select date</span>'; 1961815440faSAtari911 $html .= '<span class="date-arrow">▼</span>'; 1962815440faSAtari911 $html .= '</button>'; 1963815440faSAtari911 $html .= '<div class="date-dropdown" id="date-dropdown-' . $calId . '"></div>'; 1964815440faSAtari911 $html .= '</div>'; 196519378907SAtari911 $html .= '</div>'; 196619378907SAtari911 19671d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 1968da206178SAtari911 $html .= '<label class="field-label-compact"> End Date</label>'; 1969815440faSAtari911 $html .= '<div class="date-picker-wrapper">'; 1970815440faSAtari911 $html .= '<input type="hidden" id="event-end-date-' . $calId . '" name="endDate" value="">'; 1971815440faSAtari911 $html .= '<button type="button" class="custom-date-picker input-sleek input-compact" id="end-date-picker-btn-' . $calId . '" data-target="event-end-date-' . $calId . '" data-type="end">'; 1972815440faSAtari911 $html .= '<span class="date-display">Optional</span>'; 1973815440faSAtari911 $html .= '<span class="date-arrow">▼</span>'; 1974815440faSAtari911 $html .= '</button>'; 1975815440faSAtari911 $html .= '<div class="date-dropdown" id="end-date-dropdown-' . $calId . '"></div>'; 1976815440faSAtari911 $html .= '</div>'; 197719378907SAtari911 $html .= '</div>'; 197819378907SAtari911 19791d05cddcSAtari911 $html .= '</div>'; // End row 198019378907SAtari911 19811d05cddcSAtari911 // 4. IS REPEATING CHECKBOX 19821d05cddcSAtari911 $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">'; 19831d05cddcSAtari911 $html .= '<label class="checkbox-label checkbox-label-compact">'; 198487ac9bf3SAtari911 $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">'; 1985da206178SAtari911 $html .= '<span> Repeating Event</span>'; 198687ac9bf3SAtari911 $html .= '</label>'; 198787ac9bf3SAtari911 $html .= '</div>'; 198887ac9bf3SAtari911 19891d05cddcSAtari911 // Recurring options (shown when checkbox is checked) 199096df7d3eSAtari911 $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));">'; 199187ac9bf3SAtari911 199296df7d3eSAtari911 // Row 1: Repeat every [N] [period] 199396df7d3eSAtari911 $html .= '<div class="form-row-group" style="margin-bottom:6px;">'; 19941d05cddcSAtari911 199596df7d3eSAtari911 $html .= '<div class="form-field" style="flex:0 0 auto; min-width:0;">'; 1996da206178SAtari911 $html .= '<label class="field-label-compact">Repeat every</label>'; 199796df7d3eSAtari911 $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;">'; 199896df7d3eSAtari911 $html .= '</div>'; 199996df7d3eSAtari911 200096df7d3eSAtari911 $html .= '<div class="form-field" style="flex:1; min-width:0;">'; 200196df7d3eSAtari911 $html .= '<label class="field-label-compact"> </label>'; 200296df7d3eSAtari911 $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek input-compact" onchange="updateRecurrenceOptions(\'' . $calId . '\')">'; 2003da206178SAtari911 $html .= '<option value="daily">Day(s)</option>'; 2004da206178SAtari911 $html .= '<option value="weekly">Week(s)</option>'; 2005da206178SAtari911 $html .= '<option value="monthly">Month(s)</option>'; 2006da206178SAtari911 $html .= '<option value="yearly">Year(s)</option>'; 200787ac9bf3SAtari911 $html .= '</select>'; 200887ac9bf3SAtari911 $html .= '</div>'; 200987ac9bf3SAtari911 201096df7d3eSAtari911 $html .= '</div>'; // End row 1 201196df7d3eSAtari911 201296df7d3eSAtari911 // Row 2: Weekly options - day of week checkboxes 201396df7d3eSAtari911 $html .= '<div id="weekly-options-' . $calId . '" class="weekly-options" style="display:none; margin-bottom:6px;">'; 2014da206178SAtari911 $html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">On these days:</label>'; 201596df7d3eSAtari911 $html .= '<div style="display:flex; flex-wrap:wrap; gap:2px;">'; 2016da206178SAtari911 $dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 201796df7d3eSAtari911 foreach ($dayNames as $idx => $day) { 201896df7d3eSAtari911 $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;">'; 201996df7d3eSAtari911 $html .= '<input type="checkbox" name="weekDays[]" value="' . $idx . '" style="margin-right:3px; width:12px; height:12px;">'; 202096df7d3eSAtari911 $html .= '<span>' . $day . '</span>'; 202196df7d3eSAtari911 $html .= '</label>'; 202296df7d3eSAtari911 } 202396df7d3eSAtari911 $html .= '</div>'; 202496df7d3eSAtari911 $html .= '</div>'; // End weekly options 202596df7d3eSAtari911 202696df7d3eSAtari911 // Row 3: Monthly options - day of month OR ordinal weekday 202796df7d3eSAtari911 $html .= '<div id="monthly-options-' . $calId . '" class="monthly-options" style="display:none; margin-bottom:6px;">'; 2028da206178SAtari911 $html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">Repeat on:</label>'; 202996df7d3eSAtari911 203096df7d3eSAtari911 // Radio: Day of month vs Ordinal weekday 203196df7d3eSAtari911 $html .= '<div style="margin-bottom:6px;">'; 203296df7d3eSAtari911 $html .= '<label style="display:inline-flex; align-items:center; margin-right:12px; cursor:pointer; font-size:11px;">'; 203396df7d3eSAtari911 $html .= '<input type="radio" name="monthlyType" value="dayOfMonth" checked onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">'; 2034da206178SAtari911 $html .= 'Day of month'; 203596df7d3eSAtari911 $html .= '</label>'; 203696df7d3eSAtari911 $html .= '<label style="display:inline-flex; align-items:center; cursor:pointer; font-size:11px;">'; 203796df7d3eSAtari911 $html .= '<input type="radio" name="monthlyType" value="ordinalWeekday" onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">'; 2038da206178SAtari911 $html .= 'Weekday pattern'; 203996df7d3eSAtari911 $html .= '</label>'; 204087ac9bf3SAtari911 $html .= '</div>'; 204187ac9bf3SAtari911 204296df7d3eSAtari911 // Day of month input (shown by default) 204396df7d3eSAtari911 $html .= '<div id="monthly-day-' . $calId . '" style="display:flex; align-items:center; gap:6px;">'; 2044da206178SAtari911 $html .= '<span style="font-size:11px;">Day</span>'; 204596df7d3eSAtari911 $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;">'; 2046da206178SAtari911 $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">of each month</span>'; 204796df7d3eSAtari911 $html .= '</div>'; 204896df7d3eSAtari911 204996df7d3eSAtari911 // Ordinal weekday (hidden by default) 205096df7d3eSAtari911 $html .= '<div id="monthly-ordinal-' . $calId . '" style="display:none;">'; 205196df7d3eSAtari911 $html .= '<div style="display:flex; align-items:center; gap:4px; flex-wrap:wrap;">'; 205296df7d3eSAtari911 $html .= '<select id="event-ordinal-' . $calId . '" name="ordinalWeek" class="input-sleek input-compact" style="width:auto;">'; 2053da206178SAtari911 $html .= '<option value="1">First</option>'; 2054da206178SAtari911 $html .= '<option value="2">Second</option>'; 2055da206178SAtari911 $html .= '<option value="3">Third</option>'; 2056da206178SAtari911 $html .= '<option value="4">Fourth</option>'; 2057da206178SAtari911 $html .= '<option value="5">Fifth</option>'; 2058da206178SAtari911 $html .= '<option value="-1">Last</option>'; 205996df7d3eSAtari911 $html .= '</select>'; 206096df7d3eSAtari911 $html .= '<select id="event-ordinal-day-' . $calId . '" name="ordinalDay" class="input-sleek input-compact" style="width:auto;">'; 2061da206178SAtari911 $html .= '<option value="0">Sunday</option>'; 2062da206178SAtari911 $html .= '<option value="1">Monday</option>'; 2063da206178SAtari911 $html .= '<option value="2">Tuesday</option>'; 2064da206178SAtari911 $html .= '<option value="3">Wednesday</option>'; 2065da206178SAtari911 $html .= '<option value="4">Thursday</option>'; 2066da206178SAtari911 $html .= '<option value="5">Friday</option>'; 2067da206178SAtari911 $html .= '<option value="6">Saturday</option>'; 206896df7d3eSAtari911 $html .= '</select>'; 2069da206178SAtari911 $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">of each month</span>'; 207096df7d3eSAtari911 $html .= '</div>'; 207196df7d3eSAtari911 $html .= '</div>'; 207296df7d3eSAtari911 207396df7d3eSAtari911 $html .= '</div>'; // End monthly options 207496df7d3eSAtari911 207596df7d3eSAtari911 // Row 4: End date 207696df7d3eSAtari911 $html .= '<div class="form-row-group">'; 207796df7d3eSAtari911 $html .= '<div class="form-field">'; 2078da206178SAtari911 $html .= '<label class="field-label-compact">Repeat Until (optional)</label>'; 207996df7d3eSAtari911 $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date input-compact" placeholder="Optional">'; 2080da206178SAtari911 $html .= '<div style="font-size:9px; color:var(--text-dim, #666); margin-top:2px;">Leave empty for 1 year of events</div>'; 208196df7d3eSAtari911 $html .= '</div>'; 208296df7d3eSAtari911 $html .= '</div>'; // End row 4 208396df7d3eSAtari911 20841d05cddcSAtari911 $html .= '</div>'; // End recurring options 208587ac9bf3SAtari911 20861d05cddcSAtari911 // 5. TIME (Start & End) - COLOR (inline) 20871d05cddcSAtari911 $html .= '<div class="form-row-group">'; 20881d05cddcSAtari911 20891d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 2090da206178SAtari911 $html .= '<label class="field-label-compact"> Start Time</label>'; 2091da206178SAtari911 $html .= '<div class="time-picker-wrapper">'; 2092815440faSAtari911 // Custom time picker button instead of native select 2093815440faSAtari911 $html .= '<input type="hidden" id="event-time-' . $calId . '" name="time" value="">'; 2094815440faSAtari911 $html .= '<button type="button" class="custom-time-picker input-sleek input-compact" id="time-picker-btn-' . $calId . '" data-target="event-time-' . $calId . '" data-type="start">'; 2095815440faSAtari911 $html .= '<span class="time-display">All day</span>'; 2096815440faSAtari911 $html .= '<span class="time-arrow">▼</span>'; 2097815440faSAtari911 $html .= '</button>'; 2098815440faSAtari911 $html .= '<div class="time-dropdown" id="time-dropdown-' . $calId . '"></div>'; 209919378907SAtari911 $html .= '</div>'; 2100da206178SAtari911 $html .= '</div>'; 210119378907SAtari911 21021d05cddcSAtari911 $html .= '<div class="form-field form-field-half">'; 2103da206178SAtari911 $html .= '<label class="field-label-compact"> End Time</label>'; 2104da206178SAtari911 $html .= '<div class="time-picker-wrapper">'; 2105815440faSAtari911 // Custom end time picker 2106815440faSAtari911 $html .= '<input type="hidden" id="event-end-time-' . $calId . '" name="endTime" value="">'; 2107815440faSAtari911 $html .= '<button type="button" class="custom-time-picker input-sleek input-compact" id="end-time-picker-btn-' . $calId . '" data-target="event-end-time-' . $calId . '" data-type="end" disabled>'; 2108815440faSAtari911 $html .= '<span class="time-display">Same as start</span>'; 2109815440faSAtari911 $html .= '<span class="time-arrow">▼</span>'; 2110815440faSAtari911 $html .= '</button>'; 2111815440faSAtari911 $html .= '<div class="time-dropdown" id="end-time-dropdown-' . $calId . '"></div>'; 211219378907SAtari911 $html .= '</div>'; 2113da206178SAtari911 $html .= '</div>'; 211419378907SAtari911 21151d05cddcSAtari911 $html .= '</div>'; // End row 21161d05cddcSAtari911 21171d05cddcSAtari911 // Color field (new row) 21181d05cddcSAtari911 $html .= '<div class="form-row-group">'; 21191d05cddcSAtari911 21201d05cddcSAtari911 $html .= '<div class="form-field form-field-full">'; 2121da206178SAtari911 $html .= '<label class="field-label-compact"> Color</label>'; 21221d05cddcSAtari911 $html .= '<div class="color-picker-wrapper">'; 21231d05cddcSAtari911 $html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact color-select" onchange="updateCustomColorPicker(\'' . $calId . '\')">'; 2124da206178SAtari911 $html .= '<option value="#3498db" style="background:#3498db;color:white"> Blue</option>'; 2125da206178SAtari911 $html .= '<option value="#2ecc71" style="background:#2ecc71;color:white"> Green</option>'; 2126da206178SAtari911 $html .= '<option value="#e74c3c" style="background:#e74c3c;color:white"> Red</option>'; 2127da206178SAtari911 $html .= '<option value="#f39c12" style="background:#f39c12;color:white"> Orange</option>'; 2128da206178SAtari911 $html .= '<option value="#9b59b6" style="background:#9b59b6;color:white"> Purple</option>'; 2129da206178SAtari911 $html .= '<option value="#e91e63" style="background:#e91e63;color:white"> Pink</option>'; 2130da206178SAtari911 $html .= '<option value="#1abc9c" style="background:#1abc9c;color:white"> Teal</option>'; 2131da206178SAtari911 $html .= '<option value="custom"> Custom...</option>'; 21321d05cddcSAtari911 $html .= '</select>'; 21331d05cddcSAtari911 $html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input color-picker-compact" value="#3498db" onchange="updateColorFromPicker(\'' . $calId . '\')">'; 21341d05cddcSAtari911 $html .= '</div>'; 213519378907SAtari911 $html .= '</div>'; 213619378907SAtari911 21371d05cddcSAtari911 $html .= '</div>'; // End row 21381d05cddcSAtari911 21391d05cddcSAtari911 // Task checkbox 21401d05cddcSAtari911 $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">'; 21411d05cddcSAtari911 $html .= '<label class="checkbox-label checkbox-label-compact">'; 21421d05cddcSAtari911 $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">'; 2143da206178SAtari911 $html .= '<span> This is a task (can be checked off)</span>'; 21441d05cddcSAtari911 $html .= '</label>'; 214519378907SAtari911 $html .= '</div>'; 214619378907SAtari911 214719378907SAtari911 // Action buttons 214819378907SAtari911 $html .= '<div class="dialog-actions-sleek">'; 2149da206178SAtari911 $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>'; 2150da206178SAtari911 $html .= '<button type="submit" class="btn-sleek btn-save-sleek"> Save</button>'; 215119378907SAtari911 $html .= '</div>'; 215219378907SAtari911 215319378907SAtari911 $html .= '</form>'; 215419378907SAtari911 $html .= '</div>'; 215519378907SAtari911 $html .= '</div>'; 215619378907SAtari911 215719378907SAtari911 return $html; 215819378907SAtari911 } 215919378907SAtari911 21609ccd446eSAtari911 private function renderMonthPicker($calId, $year, $month, $namespace, $theme = 'matrix', $themeStyles = null) { 21619ccd446eSAtari911 // Fallback to default theme if not provided 21629ccd446eSAtari911 if ($themeStyles === null) { 21639ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 21649ccd446eSAtari911 } 21659ccd446eSAtari911 21669ccd446eSAtari911 $themeClass = 'calendar-theme-' . $theme; 21679ccd446eSAtari911 21689ccd446eSAtari911 $html = '<div class="month-picker-overlay ' . $themeClass . '" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">'; 216987ac9bf3SAtari911 $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">'; 217087ac9bf3SAtari911 $html .= '<h4>Jump to Month</h4>'; 217187ac9bf3SAtari911 217287ac9bf3SAtari911 $html .= '<div class="month-picker-selects">'; 217387ac9bf3SAtari911 $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">'; 217487ac9bf3SAtari911 $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 217587ac9bf3SAtari911 for ($m = 1; $m <= 12; $m++) { 217687ac9bf3SAtari911 $selected = ($m == $month) ? ' selected' : ''; 217787ac9bf3SAtari911 $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>'; 217887ac9bf3SAtari911 } 217987ac9bf3SAtari911 $html .= '</select>'; 218087ac9bf3SAtari911 218187ac9bf3SAtari911 $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">'; 218287ac9bf3SAtari911 $currentYear = (int)date('Y'); 218387ac9bf3SAtari911 for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) { 218487ac9bf3SAtari911 $selected = ($y == $year) ? ' selected' : ''; 218587ac9bf3SAtari911 $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>'; 218687ac9bf3SAtari911 } 218787ac9bf3SAtari911 $html .= '</select>'; 218887ac9bf3SAtari911 $html .= '</div>'; 218987ac9bf3SAtari911 219087ac9bf3SAtari911 $html .= '<div class="month-picker-actions">'; 219187ac9bf3SAtari911 $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>'; 219287ac9bf3SAtari911 $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>'; 219387ac9bf3SAtari911 $html .= '</div>'; 219487ac9bf3SAtari911 219587ac9bf3SAtari911 $html .= '</div>'; 219687ac9bf3SAtari911 $html .= '</div>'; 219787ac9bf3SAtari911 219887ac9bf3SAtari911 return $html; 219987ac9bf3SAtari911 } 220087ac9bf3SAtari911 22019ccd446eSAtari911 private function renderDescription($description, $themeStyles = null) { 220219378907SAtari911 if (empty($description)) { 220319378907SAtari911 return ''; 220419378907SAtari911 } 220519378907SAtari911 22069ccd446eSAtari911 // Get theme for link colors if not provided 22079ccd446eSAtari911 if ($themeStyles === null) { 22089ccd446eSAtari911 $theme = $this->getSidebarTheme(); 22099ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 22109ccd446eSAtari911 } 22119ccd446eSAtari911 22129ccd446eSAtari911 $linkColor = ''; 22139ccd446eSAtari911 $linkStyle = ' class="cal-link"'; 22149ccd446eSAtari911 2215e3a9f44cSAtari911 // Token-based parsing to avoid escaping issues 2216e3a9f44cSAtari911 $rendered = $description; 2217e3a9f44cSAtari911 $tokens = array(); 2218e3a9f44cSAtari911 $tokenIndex = 0; 221919378907SAtari911 2220e3a9f44cSAtari911 // Convert DokuWiki image syntax {{image.jpg}} to tokens 2221e3a9f44cSAtari911 $pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/'; 2222e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 2223e3a9f44cSAtari911 foreach ($matches as $match) { 2224e3a9f44cSAtari911 $imagePath = trim($match[1]); 2225e3a9f44cSAtari911 $alt = isset($match[2]) ? trim($match[2]) : ''; 222619378907SAtari911 2227e3a9f44cSAtari911 // Handle external URLs 222819378907SAtari911 if (preg_match('/^https?:\/\//', $imagePath)) { 2229e3a9f44cSAtari911 $imageHtml = '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 2230e3a9f44cSAtari911 } else { 223119378907SAtari911 // Handle internal DokuWiki images 223219378907SAtari911 $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath); 2233e3a9f44cSAtari911 $imageHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 2234e3a9f44cSAtari911 } 223519378907SAtari911 2236e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 2237e3a9f44cSAtari911 $tokens[$tokenIndex] = $imageHtml; 2238e3a9f44cSAtari911 $tokenIndex++; 2239e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 2240e3a9f44cSAtari911 } 2241e3a9f44cSAtari911 2242e3a9f44cSAtari911 // Convert DokuWiki link syntax [[link|text]] to tokens 2243e3a9f44cSAtari911 $pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/'; 2244e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 2245e3a9f44cSAtari911 foreach ($matches as $match) { 2246e3a9f44cSAtari911 $link = trim($match[1]); 2247e3a9f44cSAtari911 $text = isset($match[2]) ? trim($match[2]) : $link; 224819378907SAtari911 224919378907SAtari911 // Handle external URLs 225019378907SAtari911 if (preg_match('/^https?:\/\//', $link)) { 22519ccd446eSAtari911 $linkHtml = '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>'; 2252e3a9f44cSAtari911 } else { 225387ac9bf3SAtari911 // Handle internal DokuWiki links with section anchors 225487ac9bf3SAtari911 $parts = explode('#', $link, 2); 225587ac9bf3SAtari911 $pagePart = $parts[0]; 225687ac9bf3SAtari911 $sectionPart = isset($parts[1]) ? '#' . $parts[1] : ''; 225787ac9bf3SAtari911 225887ac9bf3SAtari911 $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart; 22599ccd446eSAtari911 $linkHtml = '<a href="' . $wikiUrl . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>'; 226019378907SAtari911 } 226119378907SAtari911 2262e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 2263e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 2264e3a9f44cSAtari911 $tokenIndex++; 2265e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 2266e3a9f44cSAtari911 } 226719378907SAtari911 2268e3a9f44cSAtari911 // Convert markdown-style links [text](url) to tokens 2269e3a9f44cSAtari911 $pattern = '/\[([^\]]+)\]\(([^)]+)\)/'; 2270e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 2271e3a9f44cSAtari911 foreach ($matches as $match) { 2272e3a9f44cSAtari911 $text = trim($match[1]); 2273e3a9f44cSAtari911 $url = trim($match[2]); 227419378907SAtari911 2275e3a9f44cSAtari911 if (preg_match('/^https?:\/\//', $url)) { 22769ccd446eSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>'; 2277e3a9f44cSAtari911 } else { 22789ccd446eSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '"' . $linkStyle . '>' . htmlspecialchars($text) . '</a>'; 2279e3a9f44cSAtari911 } 2280e3a9f44cSAtari911 2281e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 2282e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 2283e3a9f44cSAtari911 $tokenIndex++; 2284e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 2285e3a9f44cSAtari911 } 2286e3a9f44cSAtari911 2287e3a9f44cSAtari911 // Convert plain URLs to tokens 2288e3a9f44cSAtari911 $pattern = '/(https?:\/\/[^\s<]+)/'; 2289e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 2290e3a9f44cSAtari911 foreach ($matches as $match) { 2291e3a9f44cSAtari911 $url = $match[1]; 22929ccd446eSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer"' . $linkStyle . '>' . htmlspecialchars($url) . '</a>'; 2293e3a9f44cSAtari911 2294e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 2295e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 2296e3a9f44cSAtari911 $tokenIndex++; 2297e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 2298e3a9f44cSAtari911 } 2299e3a9f44cSAtari911 2300e3a9f44cSAtari911 // NOW escape HTML (tokens are protected) 2301e3a9f44cSAtari911 $rendered = htmlspecialchars($rendered); 2302e3a9f44cSAtari911 2303e3a9f44cSAtari911 // Convert newlines to <br> 2304e3a9f44cSAtari911 $rendered = nl2br($rendered); 2305e3a9f44cSAtari911 2306e3a9f44cSAtari911 // DokuWiki text formatting 2307e3a9f44cSAtari911 // Bold: **text** or __text__ 23089ccd446eSAtari911 $boldStyle = ''; 2309e3a9f44cSAtari911 $rendered = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $rendered); 2310e3a9f44cSAtari911 $rendered = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $rendered); 2311e3a9f44cSAtari911 2312e3a9f44cSAtari911 // Italic: //text// 2313e3a9f44cSAtari911 $rendered = preg_replace('/\/\/(.+?)\/\//', '<em>$1</em>', $rendered); 2314e3a9f44cSAtari911 2315e3a9f44cSAtari911 // Strikethrough: <del>text</del> 2316e3a9f44cSAtari911 $rendered = preg_replace('/<del>(.+?)<\/del>/', '<del>$1</del>', $rendered); 2317e3a9f44cSAtari911 2318e3a9f44cSAtari911 // Monospace: ''text'' 2319e3a9f44cSAtari911 $rendered = preg_replace('/''(.+?)''/', '<code>$1</code>', $rendered); 2320e3a9f44cSAtari911 2321e3a9f44cSAtari911 // Subscript: <sub>text</sub> 2322e3a9f44cSAtari911 $rendered = preg_replace('/<sub>(.+?)<\/sub>/', '<sub>$1</sub>', $rendered); 2323e3a9f44cSAtari911 2324e3a9f44cSAtari911 // Superscript: <sup>text</sup> 2325e3a9f44cSAtari911 $rendered = preg_replace('/<sup>(.+?)<\/sup>/', '<sup>$1</sup>', $rendered); 2326e3a9f44cSAtari911 2327e3a9f44cSAtari911 // Restore tokens 2328e3a9f44cSAtari911 foreach ($tokens as $i => $html) { 2329e3a9f44cSAtari911 $rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered); 2330e3a9f44cSAtari911 } 233119378907SAtari911 233219378907SAtari911 return $rendered; 233319378907SAtari911 } 233419378907SAtari911 233519378907SAtari911 private function loadEvents($namespace, $year, $month) { 23362866e827SAtari911 $dataDir = $this->metaDir(); 233719378907SAtari911 if ($namespace) { 233819378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 233919378907SAtari911 } 234019378907SAtari911 $dataDir .= 'calendar/'; 234119378907SAtari911 234219378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 234319378907SAtari911 234419378907SAtari911 if (file_exists($eventFile)) { 234519378907SAtari911 $json = file_get_contents($eventFile); 234619378907SAtari911 return json_decode($json, true); 234719378907SAtari911 } 234819378907SAtari911 234919378907SAtari911 return array(); 235019378907SAtari911 } 2351e3a9f44cSAtari911 23522866e827SAtari911 private function loadEventsMultiNamespace($namespaces, $year, $month, $excludeList = []) { 2353e3a9f44cSAtari911 // Check for wildcard pattern (namespace:*) 2354e3a9f44cSAtari911 if (preg_match('/^(.+):\*$/', $namespaces, $matches)) { 2355e3a9f44cSAtari911 $baseNamespace = $matches[1]; 23562866e827SAtari911 return $this->loadEventsWildcard($baseNamespace, $year, $month, $excludeList); 2357e3a9f44cSAtari911 } 2358e3a9f44cSAtari911 2359e3a9f44cSAtari911 // Check for root wildcard (just *) 2360e3a9f44cSAtari911 if ($namespaces === '*') { 23612866e827SAtari911 return $this->loadEventsWildcard('', $year, $month, $excludeList); 2362e3a9f44cSAtari911 } 2363e3a9f44cSAtari911 2364e3a9f44cSAtari911 // Parse namespace list (semicolon separated) 2365e3a9f44cSAtari911 // e.g., "team:projects;personal;work:tasks" = three namespaces 2366e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespaces)); 2367e3a9f44cSAtari911 2368e3a9f44cSAtari911 // Load events from all namespaces 2369e3a9f44cSAtari911 $allEvents = array(); 2370e3a9f44cSAtari911 foreach ($namespaceList as $ns) { 2371e3a9f44cSAtari911 $ns = trim($ns); 2372e3a9f44cSAtari911 if (empty($ns)) continue; 2373e3a9f44cSAtari911 23742866e827SAtari911 // Skip excluded namespaces 23752866e827SAtari911 if ($this->isNamespaceExcluded($ns, $excludeList)) continue; 23762866e827SAtari911 23772866e827SAtari911 // ACL check: skip namespaces user cannot read 23782866e827SAtari911 if (!$this->checkNamespaceRead($ns)) continue; 23792866e827SAtari911 2380e3a9f44cSAtari911 $events = $this->loadEvents($ns, $year, $month); 2381e3a9f44cSAtari911 2382e3a9f44cSAtari911 // Add namespace tag to each event 2383e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 2384e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 2385e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 2386e3a9f44cSAtari911 } 2387e3a9f44cSAtari911 foreach ($dayEvents as $event) { 2388e3a9f44cSAtari911 $event['_namespace'] = $ns; 2389e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 2390e3a9f44cSAtari911 } 2391e3a9f44cSAtari911 } 2392e3a9f44cSAtari911 } 2393e3a9f44cSAtari911 2394e3a9f44cSAtari911 return $allEvents; 2395e3a9f44cSAtari911 } 2396e3a9f44cSAtari911 23972866e827SAtari911 private function loadEventsWildcard($baseNamespace, $year, $month, $excludeList = []) { 23982866e827SAtari911 $metaDir = $this->metaDir(); 23992866e827SAtari911 $dataDir = $metaDir; 2400e3a9f44cSAtari911 if ($baseNamespace) { 2401e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 2402e3a9f44cSAtari911 } 2403e3a9f44cSAtari911 2404e3a9f44cSAtari911 $allEvents = array(); 2405e3a9f44cSAtari911 24062866e827SAtari911 // Load events from the base namespace itself 2407e3a9f44cSAtari911 if (empty($baseNamespace)) { 2408e3a9f44cSAtari911 $events = $this->loadEvents('', $year, $month); 2409e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 2410e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 2411e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 2412e3a9f44cSAtari911 } 2413e3a9f44cSAtari911 foreach ($dayEvents as $event) { 2414e3a9f44cSAtari911 $event['_namespace'] = ''; 2415e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 2416e3a9f44cSAtari911 } 2417e3a9f44cSAtari911 } 2418e3a9f44cSAtari911 } else { 24192866e827SAtari911 if (!$this->isNamespaceExcluded($baseNamespace, $excludeList) && $this->checkNamespaceRead($baseNamespace)) { 2420e3a9f44cSAtari911 $events = $this->loadEvents($baseNamespace, $year, $month); 2421e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 2422e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 2423e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 2424e3a9f44cSAtari911 } 2425e3a9f44cSAtari911 foreach ($dayEvents as $event) { 2426e3a9f44cSAtari911 $event['_namespace'] = $baseNamespace; 2427e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 2428e3a9f44cSAtari911 } 2429e3a9f44cSAtari911 } 2430e3a9f44cSAtari911 } 24312866e827SAtari911 } 2432e3a9f44cSAtari911 24332866e827SAtari911 // Find all calendar directories efficiently using iterative glob 24342866e827SAtari911 // This avoids recursing into every directory in data/meta (thousands on large wikis) 24352866e827SAtari911 $this->findCalendarNamespaces($dataDir, $metaDir, $year, $month, $allEvents, $excludeList); 2436e3a9f44cSAtari911 2437e3a9f44cSAtari911 return $allEvents; 2438e3a9f44cSAtari911 } 2439e3a9f44cSAtari911 24402866e827SAtari911 /** 24412866e827SAtari911 * Find namespaces with calendar data using iterative glob 24422866e827SAtari911 * Searches for 'calendar/' directories at increasing depth without 24432866e827SAtari911 * scanning every directory in data/meta 24442866e827SAtari911 */ 24452866e827SAtari911 private function findCalendarNamespaces($baseDir, $metaDir, $year, $month, &$allEvents, $excludeList = []) { 24462866e827SAtari911 if (!is_dir($baseDir)) return; 2447e3a9f44cSAtari911 24482866e827SAtari911 // Use glob at increasing depths to find 'calendar' directories 24492866e827SAtari911 // This is vastly more efficient than recursive scandir on large wikis 24502866e827SAtari911 $maxDepth = 10; 24512866e827SAtari911 $metaDirLen = strlen($metaDir); 2452e3a9f44cSAtari911 24532866e827SAtari911 for ($depth = 1; $depth <= $maxDepth; $depth++) { 24542866e827SAtari911 $pattern = $baseDir . str_repeat('*/', $depth) . 'calendar'; 24552866e827SAtari911 $calDirs = glob($pattern, GLOB_ONLYDIR); 2456e3a9f44cSAtari911 24572866e827SAtari911 if (empty($calDirs)) { 24582866e827SAtari911 // No calendar dirs at this depth or deeper - stop early 24592866e827SAtari911 // (only if we also found none at previous depths) 24602866e827SAtari911 if ($depth > 3) break; 24612866e827SAtari911 continue; 24622866e827SAtari911 } 24632866e827SAtari911 24642866e827SAtari911 foreach ($calDirs as $calDir) { 24652866e827SAtari911 // Derive namespace from the parent directory of 'calendar/' 24662866e827SAtari911 $nsDir = dirname($calDir); 24672866e827SAtari911 $relPath = substr($nsDir, $metaDirLen); 24682866e827SAtari911 $namespace = str_replace('/', ':', trim($relPath, '/')); 24692866e827SAtari911 24702866e827SAtari911 // Skip the root namespace (already handled above) 24712866e827SAtari911 if (empty($namespace)) continue; 24722866e827SAtari911 24732866e827SAtari911 // Skip excluded namespaces 24742866e827SAtari911 if ($this->isNamespaceExcluded($namespace, $excludeList)) continue; 24752866e827SAtari911 24762866e827SAtari911 // ACL check 24772866e827SAtari911 if (!$this->checkNamespaceRead($namespace)) continue; 24782866e827SAtari911 2479e3a9f44cSAtari911 $events = $this->loadEvents($namespace, $year, $month); 2480e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 2481e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 2482e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 2483e3a9f44cSAtari911 } 2484e3a9f44cSAtari911 foreach ($dayEvents as $event) { 2485e3a9f44cSAtari911 $event['_namespace'] = $namespace; 2486e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 2487e3a9f44cSAtari911 } 2488e3a9f44cSAtari911 } 2489e3a9f44cSAtari911 } 2490e3a9f44cSAtari911 } 2491e3a9f44cSAtari911 } 24921d05cddcSAtari911 24931d05cddcSAtari911 private function getAllNamespaces() { 24942866e827SAtari911 $dataDir = $this->metaDir(); 24951d05cddcSAtari911 $namespaces = []; 24961d05cddcSAtari911 24971d05cddcSAtari911 // Scan for namespaces that have calendar data 24981d05cddcSAtari911 $this->scanForCalendarNamespaces($dataDir, '', $namespaces); 24991d05cddcSAtari911 25001d05cddcSAtari911 // Sort alphabetically 25011d05cddcSAtari911 sort($namespaces); 25021d05cddcSAtari911 25031d05cddcSAtari911 return $namespaces; 25041d05cddcSAtari911 } 25051d05cddcSAtari911 25061d05cddcSAtari911 private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) { 25071d05cddcSAtari911 if (!is_dir($dir)) return; 25081d05cddcSAtari911 25091d05cddcSAtari911 $items = scandir($dir); 25101d05cddcSAtari911 foreach ($items as $item) { 25111d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 25121d05cddcSAtari911 25131d05cddcSAtari911 $path = $dir . $item; 25141d05cddcSAtari911 if (is_dir($path)) { 25151d05cddcSAtari911 // Check if this directory has a calendar subdirectory with data 25161d05cddcSAtari911 $calendarDir = $path . '/calendar/'; 25171d05cddcSAtari911 if (is_dir($calendarDir)) { 25181d05cddcSAtari911 // Check if there are any JSON files in the calendar directory 25191d05cddcSAtari911 $jsonFiles = glob($calendarDir . '*.json'); 25201d05cddcSAtari911 if (!empty($jsonFiles)) { 25211d05cddcSAtari911 // This namespace has calendar data 25221d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 25231d05cddcSAtari911 $namespaces[] = $namespace; 25241d05cddcSAtari911 } 25251d05cddcSAtari911 } 25261d05cddcSAtari911 25271d05cddcSAtari911 // Recurse into subdirectories 25281d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 25291d05cddcSAtari911 $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces); 25301d05cddcSAtari911 } 25311d05cddcSAtari911 } 25321d05cddcSAtari911 } 25331d05cddcSAtari911 25341d05cddcSAtari911 /** 25351d05cddcSAtari911 * Render new sidebar widget - Week at a glance itinerary (200px wide) 25361d05cddcSAtari911 */ 25370c3b6e81SAtari911 private function renderSidebarWidget($events, $namespace, $calId, $themeOverride = null) { 25381d05cddcSAtari911 if (empty($events)) { 2539da206178SAtari911 return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">No events this week</div>'; 25401d05cddcSAtari911 } 25411d05cddcSAtari911 25421d05cddcSAtari911 // Get important namespaces from config 25432866e827SAtari911 $configFile = $this->syncConfigPath(); 25441d05cddcSAtari911 $importantNsList = ['important']; // default 25451d05cddcSAtari911 if (file_exists($configFile)) { 25461d05cddcSAtari911 $config = include $configFile; 25471d05cddcSAtari911 if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) { 25481d05cddcSAtari911 $importantNsList = array_map('trim', explode(',', $config['important_namespaces'])); 25491d05cddcSAtari911 } 25501d05cddcSAtari911 } 25511d05cddcSAtari911 25521d05cddcSAtari911 // Calculate date ranges 25531d05cddcSAtari911 $todayStr = date('Y-m-d'); 25541d05cddcSAtari911 $tomorrowStr = date('Y-m-d', strtotime('+1 day')); 25559ccd446eSAtari911 25569ccd446eSAtari911 // Get week start preference and calculate week range 25579ccd446eSAtari911 $weekStartDay = $this->getWeekStartDay(); 25589ccd446eSAtari911 25599ccd446eSAtari911 if ($weekStartDay === 'monday') { 25609ccd446eSAtari911 // Monday start 25611d05cddcSAtari911 $weekStart = date('Y-m-d', strtotime('monday this week')); 25621d05cddcSAtari911 $weekEnd = date('Y-m-d', strtotime('sunday this week')); 25639ccd446eSAtari911 } else { 25649ccd446eSAtari911 // Sunday start (default - US/Canada standard) 25659ccd446eSAtari911 $today = date('w'); // 0 (Sun) to 6 (Sat) 25669ccd446eSAtari911 if ($today == 0) { 25679ccd446eSAtari911 // Today is Sunday 25689ccd446eSAtari911 $weekStart = date('Y-m-d'); 25699ccd446eSAtari911 } else { 25709ccd446eSAtari911 // Monday-Saturday: go back to last Sunday 25719ccd446eSAtari911 $weekStart = date('Y-m-d', strtotime('-' . $today . ' days')); 25729ccd446eSAtari911 } 25739ccd446eSAtari911 $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days')); 25749ccd446eSAtari911 } 25751d05cddcSAtari911 25761d05cddcSAtari911 // Group events by category 25771d05cddcSAtari911 $todayEvents = []; 25781d05cddcSAtari911 $tomorrowEvents = []; 25791d05cddcSAtari911 $importantEvents = []; 25801d05cddcSAtari911 $weekEvents = []; // For week grid 25811d05cddcSAtari911 25821d05cddcSAtari911 // Process all events 25831d05cddcSAtari911 foreach ($events as $dateKey => $dayEvents) { 25849ccd446eSAtari911 // Detect conflicts for events on this day 25859ccd446eSAtari911 $eventsWithConflicts = $this->detectTimeConflicts($dayEvents); 25861d05cddcSAtari911 25879ccd446eSAtari911 foreach ($eventsWithConflicts as $event) { 25889ccd446eSAtari911 // Always categorize Today and Tomorrow regardless of week boundaries 25899ccd446eSAtari911 if ($dateKey === $todayStr) { 25909ccd446eSAtari911 $todayEvents[] = array_merge($event, ['date' => $dateKey]); 25919ccd446eSAtari911 } 25929ccd446eSAtari911 if ($dateKey === $tomorrowStr) { 25939ccd446eSAtari911 $tomorrowEvents[] = array_merge($event, ['date' => $dateKey]); 25949ccd446eSAtari911 } 25959ccd446eSAtari911 25969ccd446eSAtari911 // Process week grid events (only for current week) 25971d05cddcSAtari911 if ($dateKey >= $weekStart && $dateKey <= $weekEnd) { 25989ccd446eSAtari911 // Initialize week grid day if not exists 25991d05cddcSAtari911 if (!isset($weekEvents[$dateKey])) { 26001d05cddcSAtari911 $weekEvents[$dateKey] = []; 26011d05cddcSAtari911 } 26021d05cddcSAtari911 26031d05cddcSAtari911 // Pre-render DokuWiki syntax to HTML for JavaScript display 26041d05cddcSAtari911 $eventWithHtml = $event; 26051d05cddcSAtari911 if (isset($event['title'])) { 26061d05cddcSAtari911 $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']); 26071d05cddcSAtari911 } 26081d05cddcSAtari911 if (isset($event['description'])) { 26091d05cddcSAtari911 $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']); 26101d05cddcSAtari911 } 26111d05cddcSAtari911 $weekEvents[$dateKey][] = $eventWithHtml; 26121d05cddcSAtari911 } 26131d05cddcSAtari911 26141d05cddcSAtari911 // Check if this is an important namespace 26151d05cddcSAtari911 $eventNs = isset($event['namespace']) ? $event['namespace'] : ''; 26161d05cddcSAtari911 $isImportant = false; 26171d05cddcSAtari911 foreach ($importantNsList as $impNs) { 26181d05cddcSAtari911 if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) { 26191d05cddcSAtari911 $isImportant = true; 26201d05cddcSAtari911 break; 26211d05cddcSAtari911 } 26221d05cddcSAtari911 } 26231d05cddcSAtari911 26249ccd446eSAtari911 // Important events: show from today through next 2 weeks 26259ccd446eSAtari911 if ($isImportant && $dateKey >= $todayStr) { 26261d05cddcSAtari911 $importantEvents[] = array_merge($event, ['date' => $dateKey]); 26271d05cddcSAtari911 } 26281d05cddcSAtari911 } 26291d05cddcSAtari911 } 26309ccd446eSAtari911 26319ccd446eSAtari911 // Sort Important Events by date (earliest first) 26329ccd446eSAtari911 usort($importantEvents, function($a, $b) { 26339ccd446eSAtari911 $dateA = isset($a['date']) ? $a['date'] : ''; 26349ccd446eSAtari911 $dateB = isset($b['date']) ? $b['date'] : ''; 26359ccd446eSAtari911 26369ccd446eSAtari911 // Compare dates 26379ccd446eSAtari911 if ($dateA === $dateB) { 26389ccd446eSAtari911 // Same date - sort by time 26399ccd446eSAtari911 $timeA = isset($a['time']) ? $a['time'] : ''; 26409ccd446eSAtari911 $timeB = isset($b['time']) ? $b['time'] : ''; 26419ccd446eSAtari911 26429ccd446eSAtari911 if (empty($timeA) && !empty($timeB)) return 1; // All-day events last 26439ccd446eSAtari911 if (!empty($timeA) && empty($timeB)) return -1; 26449ccd446eSAtari911 if (empty($timeA) && empty($timeB)) return 0; 26459ccd446eSAtari911 26469ccd446eSAtari911 // Both have times 26479ccd446eSAtari911 $aMinutes = $this->timeToMinutes($timeA); 26489ccd446eSAtari911 $bMinutes = $this->timeToMinutes($timeB); 26499ccd446eSAtari911 return $aMinutes - $bMinutes; 26501d05cddcSAtari911 } 26511d05cddcSAtari911 26529ccd446eSAtari911 return strcmp($dateA, $dateB); 26539ccd446eSAtari911 }); 26549ccd446eSAtari911 26550c3b6e81SAtari911 // Get theme - prefer override from syntax parameter, fall back to admin default 26560c3b6e81SAtari911 $theme = !empty($themeOverride) ? $themeOverride : $this->getSidebarTheme(); 26579ccd446eSAtari911 $themeStyles = $this->getSidebarThemeStyles($theme); 26589ccd446eSAtari911 $themeClass = 'sidebar-' . $theme; 26599ccd446eSAtari911 26609ccd446eSAtari911 // Start building HTML - Dynamic width with default font (overflow:visible for tooltips) 26619ccd446eSAtari911 $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;">'; 26629ccd446eSAtari911 26639ccd446eSAtari911 // Inject CSS variables so the event dialog (shared component) picks up the theme 26649ccd446eSAtari911 $btnTextColor = ($theme === 'professional') ? '#fff' : $themeStyles['bg']; 26659ccd446eSAtari911 $html .= '<style> 26669ccd446eSAtari911 #sidebar-widget-' . $calId . ' { 26679ccd446eSAtari911 --background-site: ' . $themeStyles['bg'] . '; 26689ccd446eSAtari911 --background-alt: ' . $themeStyles['cell_bg'] . '; 26699ccd446eSAtari911 --background-header: ' . $themeStyles['header_bg'] . '; 26709ccd446eSAtari911 --text-primary: ' . $themeStyles['text_primary'] . '; 26719ccd446eSAtari911 --text-dim: ' . $themeStyles['text_dim'] . '; 26729ccd446eSAtari911 --text-bright: ' . $themeStyles['text_bright'] . '; 26739ccd446eSAtari911 --border-color: ' . $themeStyles['grid_border'] . '; 26749ccd446eSAtari911 --border-main: ' . $themeStyles['border'] . '; 26759ccd446eSAtari911 --cell-bg: ' . $themeStyles['cell_bg'] . '; 26769ccd446eSAtari911 --cell-today-bg: ' . $themeStyles['cell_today_bg'] . '; 26779ccd446eSAtari911 --shadow-color: ' . $themeStyles['shadow'] . '; 26789ccd446eSAtari911 --header-border: ' . $themeStyles['header_border'] . '; 26799ccd446eSAtari911 --header-shadow: ' . $themeStyles['header_shadow'] . '; 26809ccd446eSAtari911 --grid-bg: ' . $themeStyles['grid_bg'] . '; 26819ccd446eSAtari911 --btn-text: ' . $btnTextColor . '; 26827e8ea635SAtari911 --pastdue-color: ' . $themeStyles['pastdue_color'] . '; 26837e8ea635SAtari911 --pastdue-bg: ' . $themeStyles['pastdue_bg'] . '; 26847e8ea635SAtari911 --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . '; 26857e8ea635SAtari911 --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . '; 26867e8ea635SAtari911 --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . '; 26877e8ea635SAtari911 --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . '; 26887e8ea635SAtari911 --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . '; 26899ccd446eSAtari911 } 26909ccd446eSAtari911 </style>'; 26919ccd446eSAtari911 26929ccd446eSAtari911 // Add sparkle effect for pink theme 26939ccd446eSAtari911 if ($theme === 'pink') { 26949ccd446eSAtari911 $html .= '<style> 26959ccd446eSAtari911 @keyframes sparkle-' . $calId . ' { 26969ccd446eSAtari911 0% { 26979ccd446eSAtari911 opacity: 0; 26989ccd446eSAtari911 transform: translate(0, 0) scale(0) rotate(0deg); 26999ccd446eSAtari911 } 27009ccd446eSAtari911 50% { 27019ccd446eSAtari911 opacity: 1; 27029ccd446eSAtari911 transform: translate(var(--tx), var(--ty)) scale(1) rotate(180deg); 27039ccd446eSAtari911 } 27049ccd446eSAtari911 100% { 27059ccd446eSAtari911 opacity: 0; 27069ccd446eSAtari911 transform: translate(calc(var(--tx) * 2), calc(var(--ty) * 2)) scale(0) rotate(360deg); 27079ccd446eSAtari911 } 27089ccd446eSAtari911 } 27099ccd446eSAtari911 27109ccd446eSAtari911 @keyframes pulse-glow-' . $calId . ' { 27119ccd446eSAtari911 0%, 100% { box-shadow: 0 0 10px rgba(255, 20, 147, 0.4); } 27129ccd446eSAtari911 50% { box-shadow: 0 0 25px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4); } 27139ccd446eSAtari911 } 27149ccd446eSAtari911 27159ccd446eSAtari911 @keyframes shimmer-' . $calId . ' { 27169ccd446eSAtari911 0% { background-position: -200% center; } 27179ccd446eSAtari911 100% { background-position: 200% center; } 27189ccd446eSAtari911 } 27199ccd446eSAtari911 27209ccd446eSAtari911 .sidebar-pink { 27219ccd446eSAtari911 animation: pulse-glow-' . $calId . ' 3s ease-in-out infinite; 27229ccd446eSAtari911 } 27239ccd446eSAtari911 27249ccd446eSAtari911 .sidebar-pink:hover { 27259ccd446eSAtari911 box-shadow: 0 0 30px rgba(255, 20, 147, 0.9), 0 0 50px rgba(255, 20, 147, 0.5) !important; 27269ccd446eSAtari911 } 27279ccd446eSAtari911 27289ccd446eSAtari911 .sparkle-' . $calId . ' { 27299ccd446eSAtari911 position: absolute; 27309ccd446eSAtari911 pointer-events: none; 27319ccd446eSAtari911 font-size: 20px; 27329ccd446eSAtari911 z-index: 1000; 27339ccd446eSAtari911 animation: sparkle-' . $calId . ' 1s ease-out forwards; 27349ccd446eSAtari911 filter: drop-shadow(0 0 3px rgba(255, 20, 147, 0.8)); 27359ccd446eSAtari911 } 27369ccd446eSAtari911 </style>'; 27379ccd446eSAtari911 27389ccd446eSAtari911 $html .= '<script> 27399ccd446eSAtari911 (function() { 27409ccd446eSAtari911 const container = document.getElementById("sidebar-widget-' . $calId . '"); 27419ccd446eSAtari911 const sparkles = ["✨", "", "", "⭐", "", "", "", "", "", ""]; 27429ccd446eSAtari911 27439ccd446eSAtari911 function createSparkle(x, y) { 27449ccd446eSAtari911 const sparkle = document.createElement("div"); 27459ccd446eSAtari911 sparkle.className = "sparkle-' . $calId . '"; 27469ccd446eSAtari911 sparkle.textContent = sparkles[Math.floor(Math.random() * sparkles.length)]; 27479ccd446eSAtari911 sparkle.style.left = x + "px"; 27489ccd446eSAtari911 sparkle.style.top = y + "px"; 27499ccd446eSAtari911 27509ccd446eSAtari911 // Random direction 27519ccd446eSAtari911 const angle = Math.random() * Math.PI * 2; 27529ccd446eSAtari911 const distance = 30 + Math.random() * 40; 27539ccd446eSAtari911 sparkle.style.setProperty("--tx", Math.cos(angle) * distance + "px"); 27549ccd446eSAtari911 sparkle.style.setProperty("--ty", Math.sin(angle) * distance + "px"); 27559ccd446eSAtari911 27569ccd446eSAtari911 container.appendChild(sparkle); 27579ccd446eSAtari911 27589ccd446eSAtari911 setTimeout(() => sparkle.remove(), 1000); 27599ccd446eSAtari911 } 27609ccd446eSAtari911 27619ccd446eSAtari911 // Click sparkles 27629ccd446eSAtari911 container.addEventListener("click", function(e) { 27639ccd446eSAtari911 const rect = container.getBoundingClientRect(); 27649ccd446eSAtari911 const x = e.clientX - rect.left; 27659ccd446eSAtari911 const y = e.clientY - rect.top; 27669ccd446eSAtari911 27679ccd446eSAtari911 // Create LOTS of sparkles for maximum bling! 27689ccd446eSAtari911 for (let i = 0; i < 8; i++) { 27699ccd446eSAtari911 setTimeout(() => { 27709ccd446eSAtari911 const offsetX = x + (Math.random() - 0.5) * 30; 27719ccd446eSAtari911 const offsetY = y + (Math.random() - 0.5) * 30; 27729ccd446eSAtari911 createSparkle(offsetX, offsetY); 27739ccd446eSAtari911 }, i * 40); 27749ccd446eSAtari911 } 27759ccd446eSAtari911 }); 27769ccd446eSAtari911 27779ccd446eSAtari911 // Random auto-sparkles for extra glamour 27789ccd446eSAtari911 setInterval(() => { 27799ccd446eSAtari911 const x = Math.random() * container.offsetWidth; 27809ccd446eSAtari911 const y = Math.random() * container.offsetHeight; 27819ccd446eSAtari911 createSparkle(x, y); 27829ccd446eSAtari911 }, 3000); 27839ccd446eSAtari911 })(); 27849ccd446eSAtari911 </script>'; 27859ccd446eSAtari911 } 27861d05cddcSAtari911 27871d05cddcSAtari911 // Sanitize calId for use in JavaScript variable names (remove dashes) 27881d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); 27891d05cddcSAtari911 27901d05cddcSAtari911 // CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it 27911d05cddcSAtari911 $html .= '<script> 27921d05cddcSAtari911(function() { 27931d05cddcSAtari911 // Update clock every second 27941d05cddcSAtari911 function updateClock() { 27951d05cddcSAtari911 const now = new Date(); 27961d05cddcSAtari911 let hours = now.getHours(); 27971d05cddcSAtari911 const minutes = String(now.getMinutes()).padStart(2, "0"); 27981d05cddcSAtari911 const seconds = String(now.getSeconds()).padStart(2, "0"); 27991d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 28001d05cddcSAtari911 hours = hours % 12 || 12; 28011d05cddcSAtari911 const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm; 28021d05cddcSAtari911 const clockEl = document.getElementById("clock-' . $calId . '"); 28031d05cddcSAtari911 if (clockEl) clockEl.textContent = timeStr; 28041d05cddcSAtari911 } 28051d05cddcSAtari911 setInterval(updateClock, 1000); 28061d05cddcSAtari911 280796df7d3eSAtari911 // Weather - uses default location, click weather to get local 280896df7d3eSAtari911 var userLocationGranted = false; 280996df7d3eSAtari911 var userLat = 38.5816; // Sacramento default 281096df7d3eSAtari911 var userLon = -121.4944; 28111d05cddcSAtari911 281296df7d3eSAtari911 function fetchWeatherData(lat, lon) { 281396df7d3eSAtari911 fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "¤t_weather=true&temperature_unit=fahrenheit") 28141d05cddcSAtari911 .then(response => response.json()) 28151d05cddcSAtari911 .then(data => { 28161d05cddcSAtari911 if (data.current_weather) { 28171d05cddcSAtari911 const temp = Math.round(data.current_weather.temperature); 28181d05cddcSAtari911 const weatherCode = data.current_weather.weathercode; 28191d05cddcSAtari911 const icon = getWeatherIcon(weatherCode); 28201d05cddcSAtari911 const iconEl = document.getElementById("weather-icon-' . $calId . '"); 28211d05cddcSAtari911 const tempEl = document.getElementById("weather-temp-' . $calId . '"); 28221d05cddcSAtari911 if (iconEl) iconEl.textContent = icon; 28231d05cddcSAtari911 if (tempEl) tempEl.innerHTML = temp + "°"; 28241d05cddcSAtari911 } 28251d05cddcSAtari911 }) 28261d05cddcSAtari911 .catch(error => console.log("Weather fetch error:", error)); 282796df7d3eSAtari911 } 282896df7d3eSAtari911 282996df7d3eSAtari911 function updateWeather() { 283096df7d3eSAtari911 fetchWeatherData(userLat, userLon); 283196df7d3eSAtari911 } 283296df7d3eSAtari911 283396df7d3eSAtari911 // Click weather icon to request local weather (user gesture required) 283496df7d3eSAtari911 function requestLocalWeather() { 283596df7d3eSAtari911 if (userLocationGranted) return; 283696df7d3eSAtari911 if ("geolocation" in navigator) { 283796df7d3eSAtari911 navigator.geolocation.getCurrentPosition(function(position) { 283896df7d3eSAtari911 userLat = position.coords.latitude; 283996df7d3eSAtari911 userLon = position.coords.longitude; 284096df7d3eSAtari911 userLocationGranted = true; 284196df7d3eSAtari911 fetchWeatherData(userLat, userLon); 28421d05cddcSAtari911 }, function(error) { 284396df7d3eSAtari911 console.log("Geolocation denied, using default location"); 28441d05cddcSAtari911 }); 28451d05cddcSAtari911 } 28461d05cddcSAtari911 } 28471d05cddcSAtari911 284896df7d3eSAtari911 setTimeout(function() { 284996df7d3eSAtari911 var weatherEl = document.querySelector("#weather-icon-' . $calId . '"); 285096df7d3eSAtari911 if (weatherEl) { 285196df7d3eSAtari911 weatherEl.style.cursor = "pointer"; 285296df7d3eSAtari911 weatherEl.title = "Click for local weather"; 285396df7d3eSAtari911 weatherEl.addEventListener("click", requestLocalWeather); 285496df7d3eSAtari911 } 285596df7d3eSAtari911 }, 100); 285696df7d3eSAtari911 28571d05cddcSAtari911 function getWeatherIcon(code) { 28581d05cddcSAtari911 const icons = { 28591d05cddcSAtari911 0: "☀️", 1: "️", 2: "⛅", 3: "☁️", 28601d05cddcSAtari911 45: "️", 48: "️", 51: "️", 53: "️", 55: "️", 28611d05cddcSAtari911 61: "️", 63: "️", 65: "⛈️", 71: "️", 73: "️", 28621d05cddcSAtari911 75: "❄️", 77: "️", 80: "️", 81: "️", 82: "⛈️", 28631d05cddcSAtari911 85: "️", 86: "❄️", 95: "⛈️", 96: "⛈️", 99: "⛈️" 28641d05cddcSAtari911 }; 28651d05cddcSAtari911 return icons[code] || "️"; 28661d05cddcSAtari911 } 28671d05cddcSAtari911 28681d05cddcSAtari911 // Update weather immediately and every 10 minutes 28691d05cddcSAtari911 updateWeather(); 28701d05cddcSAtari911 setInterval(updateWeather, 600000); 28711d05cddcSAtari911})(); 28721d05cddcSAtari911</script>'; 28731d05cddcSAtari911 28741d05cddcSAtari911 // NOW add the header HTML (after JavaScript is defined) 28751d05cddcSAtari911 $todayDate = new DateTime(); 28761d05cddcSAtari911 $displayDate = $todayDate->format('D, M j, Y'); 28771d05cddcSAtari911 $currentTime = $todayDate->format('g:i:s A'); 28781d05cddcSAtari911 28799ccd446eSAtari911 $html .= '<div class="eventlist-today-header" style="background:' . $themeStyles['header_bg'] . '; border:2px solid ' . $themeStyles['header_border'] . '; box-shadow:' . $themeStyles['header_shadow'] . ';">'; 28809ccd446eSAtari911 $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '" style="color:' . $themeStyles['text_bright'] . ';">' . $currentTime . '</span>'; 28811d05cddcSAtari911 $html .= '<div class="eventlist-bottom-info">'; 28829ccd446eSAtari911 $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">️</span> <span id="weather-temp-' . $calId . '" style="color:' . $themeStyles['text_primary'] . ';">--°</span></span>'; 28839ccd446eSAtari911 $html .= '<span class="eventlist-today-date" style="color:' . $themeStyles['text_dim'] . ';">' . $displayDate . '</span>'; 28841d05cddcSAtari911 $html .= '</div>'; 28851d05cddcSAtari911 $html .= '</div>'; 28861d05cddcSAtari911 2887231d0edbSAtari911 // Get today's date for default event date 2888231d0edbSAtari911 $todayStr = date('Y-m-d'); 2889231d0edbSAtari911 28909ccd446eSAtari911 // Thin "Add Event" bar between header and week grid - theme-aware colors 28917e8ea635SAtari911 $addBtnBg = $themeStyles['cell_today_bg']; 28927e8ea635SAtari911 $addBtnHover = $themeStyles['grid_bg']; 28937e8ea635SAtari911 $addBtnTextColor = ($theme === 'professional' || $theme === 'wiki') ? 28947e8ea635SAtari911 $themeStyles['text_bright'] : $themeStyles['text_bright']; 28957e8ea635SAtari911 $addBtnShadow = ($theme === 'professional' || $theme === 'wiki') ? 28967e8ea635SAtari911 '0 2px 4px rgba(0,0,0,0.2)' : '0 0 8px ' . $themeStyles['shadow']; 28977e8ea635SAtari911 $addBtnHoverShadow = ($theme === 'professional' || $theme === 'wiki') ? 28987e8ea635SAtari911 '0 3px 6px rgba(0,0,0,0.3)' : '0 0 12px ' . $themeStyles['shadow']; 28999ccd446eSAtari911 29009ccd446eSAtari911 $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 . '\';">'; 29019ccd446eSAtari911 $addBtnTextShadow = ($theme === 'pink') ? '0 0 3px ' . $addBtnTextColor : 'none'; 2902da206178SAtari911 $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>'; 29031d05cddcSAtari911 $html .= '</div>'; 29041d05cddcSAtari911 29051d05cddcSAtari911 // Week grid (7 cells) 29069ccd446eSAtari911 $html .= $this->renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme); 29071d05cddcSAtari911 29087e8ea635SAtari911 // Section colors - derived from theme palette 29097e8ea635SAtari911 // Today: brightest accent, Tomorrow: primary accent, Important: dim/secondary accent 29107e8ea635SAtari911 if ($theme === 'matrix') { 29117e8ea635SAtari911 $todayColor = '#00ff00'; // Bright green 29127e8ea635SAtari911 $tomorrowColor = '#00cc07'; // Standard green 29137e8ea635SAtari911 $importantColor = '#00aa00'; // Dim green 29147e8ea635SAtari911 } else if ($theme === 'purple') { 29157e8ea635SAtari911 $todayColor = '#d4a5ff'; // Bright purple 29167e8ea635SAtari911 $tomorrowColor = '#9b59b6'; // Standard purple 29177e8ea635SAtari911 $importantColor = '#8e7ab8'; // Dim purple 29187e8ea635SAtari911 } else if ($theme === 'pink') { 29197e8ea635SAtari911 $todayColor = '#ff1493'; // Hot pink 29207e8ea635SAtari911 $tomorrowColor = '#ff69b4'; // Medium pink 29217e8ea635SAtari911 $importantColor = '#ff85c1'; // Light pink 29227e8ea635SAtari911 } else if ($theme === 'professional') { 29237e8ea635SAtari911 $todayColor = '#4a90e2'; // Blue accent 29247e8ea635SAtari911 $tomorrowColor = '#5ba3e6'; // Lighter blue 29257e8ea635SAtari911 $importantColor = '#7fb8ec'; // Lightest blue 29269ccd446eSAtari911 } else { 29277e8ea635SAtari911 // Wiki - section header backgrounds from template colors 29287e8ea635SAtari911 $todayColor = $themeStyles['text_bright']; // __link__ 29297e8ea635SAtari911 $tomorrowColor = $themeStyles['header_bg']; // __background_alt__ 29307e8ea635SAtari911 $importantColor = $themeStyles['header_border'];// __border__ 29319ccd446eSAtari911 } 29329ccd446eSAtari911 293396df7d3eSAtari911 // Check if there are any itinerary items 293496df7d3eSAtari911 $hasItinerary = !empty($todayEvents) || !empty($tomorrowEvents) || !empty($importantEvents); 293596df7d3eSAtari911 293696df7d3eSAtari911 // Itinerary bar (collapsible toggle) - styled like +Add bar 293796df7d3eSAtari911 $itineraryBg = $themeStyles['cell_today_bg']; 293896df7d3eSAtari911 $itineraryHover = $themeStyles['grid_bg']; 293996df7d3eSAtari911 $itineraryTextColor = ($theme === 'professional' || $theme === 'wiki') ? 294096df7d3eSAtari911 $themeStyles['text_bright'] : $themeStyles['text_bright']; 294196df7d3eSAtari911 $itineraryShadow = ($theme === 'professional' || $theme === 'wiki') ? 294296df7d3eSAtari911 '0 2px 4px rgba(0,0,0,0.2)' : '0 0 8px ' . $themeStyles['shadow']; 294396df7d3eSAtari911 $itineraryHoverShadow = ($theme === 'professional' || $theme === 'wiki') ? 294496df7d3eSAtari911 '0 3px 6px rgba(0,0,0,0.3)' : '0 0 12px ' . $themeStyles['shadow']; 294596df7d3eSAtari911 $itineraryTextShadow = ($theme === 'pink') ? '0 0 3px ' . $itineraryTextColor : 'none'; 294696df7d3eSAtari911 294796df7d3eSAtari911 // Sanitize calId for JavaScript 294896df7d3eSAtari911 $jsCalId = str_replace('-', '_', $calId); 294996df7d3eSAtari911 295096df7d3eSAtari911 // Get itinerary default state from settings 295196df7d3eSAtari911 $itineraryDefaultCollapsed = $this->getItineraryCollapsed(); 295296df7d3eSAtari911 $arrowDefaultStyle = $itineraryDefaultCollapsed ? 'transform:rotate(-90deg);' : ''; 295396df7d3eSAtari911 $contentDefaultStyle = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : ''; 295496df7d3eSAtari911 295596df7d3eSAtari911 $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 . '\';">'; 295696df7d3eSAtari911 $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>'; 295796df7d3eSAtari911 $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>'; 295896df7d3eSAtari911 $html .= '</div>'; 295996df7d3eSAtari911 296096df7d3eSAtari911 // Itinerary content container (collapsible) 296196df7d3eSAtari911 $html .= '<div id="itinerary-content-' . $calId . '" style="transition:max-height 0.3s ease-out, opacity 0.2s ease-out; overflow:hidden; ' . $contentDefaultStyle . '">'; 296296df7d3eSAtari911 29639ccd446eSAtari911 // Today section 29641d05cddcSAtari911 if (!empty($todayEvents)) { 2965da206178SAtari911 $html .= $this->renderSidebarSection('Today', $todayEvents, $todayColor, $calId, $themeStyles, $theme, $importantNsList); 29661d05cddcSAtari911 } 29671d05cddcSAtari911 29689ccd446eSAtari911 // Tomorrow section 29691d05cddcSAtari911 if (!empty($tomorrowEvents)) { 2970da206178SAtari911 $html .= $this->renderSidebarSection('Tomorrow', $tomorrowEvents, $tomorrowColor, $calId, $themeStyles, $theme, $importantNsList); 29711d05cddcSAtari911 } 29721d05cddcSAtari911 29739ccd446eSAtari911 // Important events section 29741d05cddcSAtari911 if (!empty($importantEvents)) { 2975da206178SAtari911 $html .= $this->renderSidebarSection('Important Events', $importantEvents, $importantColor, $calId, $themeStyles, $theme, $importantNsList); 29761d05cddcSAtari911 } 29771d05cddcSAtari911 297896df7d3eSAtari911 // Empty state if no itinerary items 297996df7d3eSAtari911 if (!$hasItinerary) { 2980da206178SAtari911 $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>'; 298196df7d3eSAtari911 } 298296df7d3eSAtari911 298396df7d3eSAtari911 $html .= '</div>'; // Close itinerary-content 298496df7d3eSAtari911 298596df7d3eSAtari911 // Get itinerary default state from settings 298696df7d3eSAtari911 $itineraryDefaultCollapsed = $this->getItineraryCollapsed(); 298796df7d3eSAtari911 $itineraryExpandedDefault = $itineraryDefaultCollapsed ? 'false' : 'true'; 298896df7d3eSAtari911 $itineraryArrowDefault = $itineraryDefaultCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)'; 298996df7d3eSAtari911 $itineraryContentDefault = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : 'max-height:none;'; 299096df7d3eSAtari911 299196df7d3eSAtari911 // JavaScript for toggling itinerary 299296df7d3eSAtari911 $html .= '<script> 299396df7d3eSAtari911 (function() { 299496df7d3eSAtari911 let itineraryExpanded_' . $jsCalId . ' = ' . $itineraryExpandedDefault . '; 299596df7d3eSAtari911 299696df7d3eSAtari911 window.toggleItinerary_' . $jsCalId . ' = function() { 299796df7d3eSAtari911 const content = document.getElementById("itinerary-content-' . $calId . '"); 299896df7d3eSAtari911 const arrow = document.getElementById("itinerary-arrow-' . $calId . '"); 299996df7d3eSAtari911 300096df7d3eSAtari911 if (itineraryExpanded_' . $jsCalId . ') { 300196df7d3eSAtari911 // Collapse 300296df7d3eSAtari911 content.style.maxHeight = "0px"; 300396df7d3eSAtari911 content.style.opacity = "0"; 300496df7d3eSAtari911 arrow.style.transform = "rotate(-90deg)"; 300596df7d3eSAtari911 itineraryExpanded_' . $jsCalId . ' = false; 300696df7d3eSAtari911 } else { 300796df7d3eSAtari911 // Expand 300896df7d3eSAtari911 content.style.maxHeight = content.scrollHeight + "px"; 300996df7d3eSAtari911 content.style.opacity = "1"; 301096df7d3eSAtari911 arrow.style.transform = "rotate(0deg)"; 301196df7d3eSAtari911 itineraryExpanded_' . $jsCalId . ' = true; 301296df7d3eSAtari911 301396df7d3eSAtari911 // After transition, set to auto for dynamic content 301496df7d3eSAtari911 setTimeout(function() { 301596df7d3eSAtari911 if (itineraryExpanded_' . $jsCalId . ') { 301696df7d3eSAtari911 content.style.maxHeight = "none"; 301796df7d3eSAtari911 } 301896df7d3eSAtari911 }, 300); 301996df7d3eSAtari911 } 302096df7d3eSAtari911 }; 302196df7d3eSAtari911 302296df7d3eSAtari911 // Initialize based on default state 302396df7d3eSAtari911 const content = document.getElementById("itinerary-content-' . $calId . '"); 302496df7d3eSAtari911 const arrow = document.getElementById("itinerary-arrow-' . $calId . '"); 302596df7d3eSAtari911 if (content && arrow) { 302696df7d3eSAtari911 if (' . $itineraryExpandedDefault . ') { 302796df7d3eSAtari911 content.style.maxHeight = "none"; 302896df7d3eSAtari911 arrow.style.transform = "rotate(0deg)"; 302996df7d3eSAtari911 } else { 303096df7d3eSAtari911 content.style.maxHeight = "0px"; 303196df7d3eSAtari911 content.style.opacity = "0"; 303296df7d3eSAtari911 arrow.style.transform = "rotate(-90deg)"; 303396df7d3eSAtari911 } 303496df7d3eSAtari911 } 303596df7d3eSAtari911 })(); 303696df7d3eSAtari911 </script>'; 303796df7d3eSAtari911 30381d05cddcSAtari911 $html .= '</div>'; 30391d05cddcSAtari911 3040231d0edbSAtari911 // Add event dialog for sidebar widget 30410c3b6e81SAtari911 $html .= $this->renderEventDialog($calId, $namespace, $theme); 3042231d0edbSAtari911 30439ccd446eSAtari911 // Add JavaScript for positioning data-tooltip elements 30449ccd446eSAtari911 $html .= '<script> 30459ccd446eSAtari911 // Position data-tooltip elements to prevent cutoff (up and to the LEFT) 30469ccd446eSAtari911 document.addEventListener("DOMContentLoaded", function() { 30479ccd446eSAtari911 const tooltipElements = document.querySelectorAll("[data-tooltip]"); 30489ccd446eSAtari911 const isPinkTheme = document.querySelector(".sidebar-pink") !== null; 30499ccd446eSAtari911 30509ccd446eSAtari911 tooltipElements.forEach(function(element) { 30519ccd446eSAtari911 element.addEventListener("mouseenter", function() { 30529ccd446eSAtari911 const rect = element.getBoundingClientRect(); 30539ccd446eSAtari911 const style = window.getComputedStyle(element, ":before"); 30549ccd446eSAtari911 30559ccd446eSAtari911 // Position above the element, aligned to LEFT (not right) 30569ccd446eSAtari911 element.style.setProperty("--tooltip-left", (rect.left - 150) + "px"); 30579ccd446eSAtari911 element.style.setProperty("--tooltip-top", (rect.top - 30) + "px"); 30589ccd446eSAtari911 30599ccd446eSAtari911 // Pink theme: position heart to the right of tooltip 30609ccd446eSAtari911 if (isPinkTheme) { 30619ccd446eSAtari911 element.style.setProperty("--heart-left", (rect.left - 150 + 210) + "px"); 30629ccd446eSAtari911 element.style.setProperty("--heart-top", (rect.top - 30) + "px"); 30639ccd446eSAtari911 } 30649ccd446eSAtari911 }); 30659ccd446eSAtari911 }); 30669ccd446eSAtari911 }); 30679ccd446eSAtari911 30689ccd446eSAtari911 // Apply custom properties to position tooltips 30699ccd446eSAtari911 const style = document.createElement("style"); 30709ccd446eSAtari911 style.textContent = ` 30719ccd446eSAtari911 [data-tooltip]:hover:before { 30729ccd446eSAtari911 left: var(--tooltip-left, 0) !important; 30739ccd446eSAtari911 top: var(--tooltip-top, 0) !important; 30749ccd446eSAtari911 } 30759ccd446eSAtari911 .sidebar-pink [data-tooltip]:hover:after { 30769ccd446eSAtari911 left: var(--heart-left, 0) !important; 30779ccd446eSAtari911 top: var(--heart-top, 0) !important; 30789ccd446eSAtari911 } 30799ccd446eSAtari911 `; 30809ccd446eSAtari911 document.head.appendChild(style); 30819ccd446eSAtari911 </script>'; 30829ccd446eSAtari911 30831d05cddcSAtari911 return $html; 30841d05cddcSAtari911 } 30851d05cddcSAtari911 30861d05cddcSAtari911 /** 30879ccd446eSAtari911 * Render compact week grid (7 cells with event bars) - Theme-aware 30881d05cddcSAtari911 */ 30899ccd446eSAtari911 private function renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme) { 30901d05cddcSAtari911 // Generate unique ID for this calendar instance - sanitize for JavaScript 30911d05cddcSAtari911 $calId = 'cal_' . substr(md5($weekStart . microtime()), 0, 8); 30921d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); // Sanitize for JS variable names 30931d05cddcSAtari911 30949ccd446eSAtari911 $html = '<div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:' . $themeStyles['grid_bg'] . '; border-bottom:2px solid ' . $themeStyles['grid_border'] . ';">'; 30951d05cddcSAtari911 30969ccd446eSAtari911 // Day names depend on week start setting 30979ccd446eSAtari911 $weekStartDay = $this->getWeekStartDay(); 30989ccd446eSAtari911 if ($weekStartDay === 'monday') { 30999ccd446eSAtari911 $dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; // Monday to Sunday 31009ccd446eSAtari911 } else { 31019ccd446eSAtari911 $dayNames = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; // Sunday to Saturday 31029ccd446eSAtari911 } 31031d05cddcSAtari911 $today = date('Y-m-d'); 31041d05cddcSAtari911 31051d05cddcSAtari911 for ($i = 0; $i < 7; $i++) { 31061d05cddcSAtari911 $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days')); 31071d05cddcSAtari911 $dayNum = date('j', strtotime($date)); 31081d05cddcSAtari911 $isToday = $date === $today; 31091d05cddcSAtari911 31101d05cddcSAtari911 $events = isset($weekEvents[$date]) ? $weekEvents[$date] : []; 31111d05cddcSAtari911 $eventCount = count($events); 31121d05cddcSAtari911 31139ccd446eSAtari911 $bgColor = $isToday ? $themeStyles['cell_today_bg'] : $themeStyles['cell_bg']; 31149ccd446eSAtari911 $textColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary']; 31151d05cddcSAtari911 $fontWeight = $isToday ? '700' : '500'; 31169ccd446eSAtari911 31179ccd446eSAtari911 // Theme-aware text shadow 31189ccd446eSAtari911 if ($theme === 'pink') { 31199ccd446eSAtari911 $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary']; 31207e8ea635SAtari911 $textShadow = $isToday ? 'text-shadow:0 0 3px ' . $glowColor . ';' : 'text-shadow:0 0 2px ' . $glowColor . ';'; 31217e8ea635SAtari911 } else if ($theme === 'matrix') { 31227e8ea635SAtari911 $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary']; 31237e8ea635SAtari911 $textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glowColor . ';'; 31247e8ea635SAtari911 } else if ($theme === 'purple') { 31257e8ea635SAtari911 $glowColor = $isToday ? $themeStyles['text_bright'] : $themeStyles['text_primary']; 31267e8ea635SAtari911 $textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glowColor . ';'; 31279ccd446eSAtari911 } else { 31287e8ea635SAtari911 $textShadow = ''; // No glow for professional/wiki 31299ccd446eSAtari911 } 31309ccd446eSAtari911 31319ccd446eSAtari911 // Border color based on theme 31329ccd446eSAtari911 $borderColor = $themeStyles['grid_border']; 31331d05cddcSAtari911 31341d05cddcSAtari911 $hasEvents = $eventCount > 0; 31351d05cddcSAtari911 $clickableStyle = $hasEvents ? 'cursor:pointer;' : ''; 31361d05cddcSAtari911 $clickHandler = $hasEvents ? ' onclick="showDayEvents_' . $jsCalId . '(\'' . $date . '\')"' : ''; 31371d05cddcSAtari911 31389ccd446eSAtari911 $html .= '<div style="background:' . $bgColor . '; padding:4px 2px; text-align:center; min-height:45px; position:relative; border:1px solid ' . $borderColor . ' !important; ' . $clickableStyle . '" ' . $clickHandler . '>'; 31391d05cddcSAtari911 31409ccd446eSAtari911 // Day letter - theme color 31419ccd446eSAtari911 $dayLetterColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary']; 31429ccd446eSAtari911 $html .= '<div style="font-size:9px; color:' . $dayLetterColor . '; font-weight:500; font-family:system-ui, sans-serif;">' . $dayNames[$i] . '</div>'; 31431d05cddcSAtari911 31441d05cddcSAtari911 // Day number 31451d05cddcSAtari911 $html .= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:2px 0; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNum . '</div>'; 31461d05cddcSAtari911 31479ccd446eSAtari911 // Event bars (max 4 visible) with theme-aware glow 31481d05cddcSAtari911 if ($eventCount > 0) { 31499ccd446eSAtari911 $showCount = min($eventCount, 4); 31501d05cddcSAtari911 for ($j = 0; $j < $showCount; $j++) { 31511d05cddcSAtari911 $event = $events[$j]; 31529ccd446eSAtari911 $color = isset($event['color']) ? $event['color'] : $themeStyles['text_primary']; 31539ccd446eSAtari911 $barShadow = $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . htmlspecialchars($color); 31549ccd446eSAtari911 $html .= '<div style="height:2px; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:' . $barShadow . ';"></div>'; 31551d05cddcSAtari911 } 31561d05cddcSAtari911 31579ccd446eSAtari911 // Show "+N more" if more than 4 - theme color 31589ccd446eSAtari911 if ($eventCount > 4) { 31599ccd446eSAtari911 $moreTextColor = $theme === 'professional' ? '#7f8c8d' : $themeStyles['text_primary']; 31609ccd446eSAtari911 $html .= '<div style="font-size:7px; color:' . $moreTextColor . '; margin-top:1px; font-family:system-ui, sans-serif;">+' . ($eventCount - 4) . '</div>'; 31611d05cddcSAtari911 } 31621d05cddcSAtari911 } 31631d05cddcSAtari911 31641d05cddcSAtari911 $html .= '</div>'; 31651d05cddcSAtari911 } 31661d05cddcSAtari911 31671d05cddcSAtari911 $html .= '</div>'; 31681d05cddcSAtari911 31699ccd446eSAtari911 // Add container for selected day events display (with unique ID) - theme-aware 31707e8ea635SAtari911 $panelBorderColor = $themeStyles['border']; 31717e8ea635SAtari911 $panelHeaderBg = $themeStyles['border']; 31727e8ea635SAtari911 $panelShadow = ($theme === 'professional' || $theme === 'wiki') ? 31737e8ea635SAtari911 '0 1px 3px rgba(0, 0, 0, 0.1)' : 31747e8ea635SAtari911 '0 0 5px ' . $themeStyles['shadow']; 31757e8ea635SAtari911 $panelContentBg = ($theme === 'professional') ? 'rgba(255, 255, 255, 0.95)' : 31769ccd446eSAtari911 ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.5)'); 31779ccd446eSAtari911 $panelHeaderShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $panelHeaderBg; 31789ccd446eSAtari911 31797e8ea635SAtari911 // Header text color - dark bg text for dark themes, white for light theme accent headers 31807e8ea635SAtari911 $panelHeaderColor = ($theme === 'matrix' || $theme === 'purple' || $theme === 'pink') ? $themeStyles['bg'] : 31817e8ea635SAtari911 (($theme === 'wiki') ? $themeStyles['text_primary'] : '#fff'); 31829ccd446eSAtari911 31837e8ea635SAtari911 $html .= '<div id="selected-day-events-' . $calId . '" style="display:none; margin:8px 4px; border-left:3px solid ' . $panelBorderColor . ($theme === 'wiki' ? '' : ' !important') . '; box-shadow:' . $panelShadow . ';">'; 31847e8ea635SAtari911 if ($theme === 'wiki') { 31859ccd446eSAtari911 $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;">'; 31861d05cddcSAtari911 $html .= '<span id="selected-day-title-' . $calId . '"></span>'; 31879ccd446eSAtari911 $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>'; 31887e8ea635SAtari911 } else { 31897e8ea635SAtari911 $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;">'; 31907e8ea635SAtari911 $html .= '<span id="selected-day-title-' . $calId . '"></span>'; 31917e8ea635SAtari911 $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>'; 31927e8ea635SAtari911 } 31931d05cddcSAtari911 $html .= '</div>'; 31949ccd446eSAtari911 $html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:' . $panelContentBg . ';"></div>'; 31951d05cddcSAtari911 $html .= '</div>'; 31961d05cddcSAtari911 31971d05cddcSAtari911 // Add JavaScript for day selection with event data 31981d05cddcSAtari911 $html .= '<script>'; 31991d05cddcSAtari911 // Sanitize calId for JavaScript variable names 32001d05cddcSAtari911 $jsCalId = str_replace('-', '_', $calId); 32011d05cddcSAtari911 $html .= 'window.weekEventsData_' . $jsCalId . ' = ' . json_encode($weekEvents) . ';'; 32029ccd446eSAtari911 32039ccd446eSAtari911 // Pass theme colors to JavaScript 32049ccd446eSAtari911 $jsThemeColors = json_encode([ 32059ccd446eSAtari911 'text_primary' => $themeStyles['text_primary'], 32069ccd446eSAtari911 'text_bright' => $themeStyles['text_bright'], 32079ccd446eSAtari911 'text_dim' => $themeStyles['text_dim'], 32087e8ea635SAtari911 'text_shadow' => ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $themeStyles['text_primary'] : 32097e8ea635SAtari911 ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $themeStyles['text_primary'] : ''), 32109ccd446eSAtari911 'event_bg' => $theme === 'professional' ? 'rgba(255, 255, 255, 0.5)' : 32119ccd446eSAtari911 ($theme === 'wiki' ? $themeStyles['cell_bg'] : 'rgba(36, 36, 36, 0.3)'), 32129ccd446eSAtari911 'border_color' => $theme === 'professional' ? 'rgba(0, 0, 0, 0.1)' : 32139ccd446eSAtari911 ($theme === 'purple' ? 'rgba(155, 89, 182, 0.2)' : 32149ccd446eSAtari911 ($theme === 'pink' ? 'rgba(255, 20, 147, 0.3)' : 32159ccd446eSAtari911 ($theme === 'wiki' ? $themeStyles['grid_border'] : 'rgba(0, 204, 7, 0.2)'))), 32169ccd446eSAtari911 'bar_shadow' => $theme === 'professional' ? '0 1px 2px rgba(0,0,0,0.2)' : 32179ccd446eSAtari911 ($theme === 'wiki' ? '0 1px 2px rgba(0,0,0,0.15)' : '0 0 3px') 32189ccd446eSAtari911 ]); 32199ccd446eSAtari911 $html .= 'window.themeColors_' . $jsCalId . ' = ' . $jsThemeColors . ';'; 32201d05cddcSAtari911 $html .= ' 32211d05cddcSAtari911 window.showDayEvents_' . $jsCalId . ' = function(dateKey) { 32221d05cddcSAtari911 const eventsData = window.weekEventsData_' . $jsCalId . '; 32231d05cddcSAtari911 const container = document.getElementById("selected-day-events-' . $calId . '"); 32241d05cddcSAtari911 const title = document.getElementById("selected-day-title-' . $calId . '"); 32251d05cddcSAtari911 const content = document.getElementById("selected-day-content-' . $calId . '"); 32261d05cddcSAtari911 32271d05cddcSAtari911 if (!eventsData[dateKey] || eventsData[dateKey].length === 0) return; 32281d05cddcSAtari911 32291d05cddcSAtari911 // Format date for display 32301d05cddcSAtari911 const dateObj = new Date(dateKey + "T00:00:00"); 32311d05cddcSAtari911 const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" }); 32321d05cddcSAtari911 const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" }); 32331d05cddcSAtari911 title.textContent = dayName + ", " + monthDay; 32341d05cddcSAtari911 32351d05cddcSAtari911 // Clear content 32361d05cddcSAtari911 content.innerHTML = ""; 32371d05cddcSAtari911 3238231d0edbSAtari911 // Sort events by time (all-day events first, then timed events chronologically) 32391d05cddcSAtari911 const sortedEvents = [...eventsData[dateKey]].sort((a, b) => { 3240231d0edbSAtari911 // All-day events (no time) go to the beginning 32411d05cddcSAtari911 if (!a.time && !b.time) return 0; 3242231d0edbSAtari911 if (!a.time) return -1; // a is all-day, comes first 3243231d0edbSAtari911 if (!b.time) return 1; // b is all-day, comes first 32441d05cddcSAtari911 32451d05cddcSAtari911 // Compare times (format: "HH:MM") 32461d05cddcSAtari911 const timeA = a.time.split(":").map(Number); 32471d05cddcSAtari911 const timeB = b.time.split(":").map(Number); 32481d05cddcSAtari911 const minutesA = timeA[0] * 60 + timeA[1]; 32491d05cddcSAtari911 const minutesB = timeB[0] * 60 + timeB[1]; 32501d05cddcSAtari911 32511d05cddcSAtari911 return minutesA - minutesB; 32521d05cddcSAtari911 }); 32531d05cddcSAtari911 32549ccd446eSAtari911 // Build events HTML with single color bar (event color only) - theme-aware 32559ccd446eSAtari911 const themeColors = window.themeColors_' . $jsCalId . '; 32561d05cddcSAtari911 sortedEvents.forEach(event => { 32579ccd446eSAtari911 const eventColor = event.color || themeColors.text_primary; 32581d05cddcSAtari911 32591d05cddcSAtari911 const eventDiv = document.createElement("div"); 32609ccd446eSAtari911 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;"; 32611d05cddcSAtari911 32621d05cddcSAtari911 let eventHTML = ""; 32631d05cddcSAtari911 32649ccd446eSAtari911 // Event assigned color bar (single bar on left) - theme-aware shadow 32659ccd446eSAtari911 const barShadow = themeColors.bar_shadow + (themeColors.bar_shadow.includes("rgba") ? "" : " " + eventColor); 32669ccd446eSAtari911 eventHTML += "<div style=\\"width:3px; align-self:stretch; background:" + eventColor + "; border-radius:1px; flex-shrink:0; box-shadow:" + barShadow + ";\\"></div>"; 32671d05cddcSAtari911 3268231d0edbSAtari911 // Content wrapper 3269231d0edbSAtari911 eventHTML += "<div style=\\"flex:1; min-width:0; display:flex; justify-content:space-between; align-items:start; gap:4px;\\">"; 32701d05cddcSAtari911 3271231d0edbSAtari911 // Left side: event details 32721d05cddcSAtari911 eventHTML += "<div style=\\"flex:1; min-width:0;\\">"; 32739ccd446eSAtari911 eventHTML += "<div style=\\"font-weight:600; color:" + themeColors.text_primary + "; word-wrap:break-word; font-family:system-ui, sans-serif; " + themeColors.text_shadow + ";\\">"; 32741d05cddcSAtari911 32751d05cddcSAtari911 // Time 32761d05cddcSAtari911 if (event.time) { 32771d05cddcSAtari911 const timeParts = event.time.split(":"); 32781d05cddcSAtari911 let hours = parseInt(timeParts[0]); 32791d05cddcSAtari911 const minutes = timeParts[1]; 32801d05cddcSAtari911 const ampm = hours >= 12 ? "PM" : "AM"; 32811d05cddcSAtari911 hours = hours % 12 || 12; 32829ccd446eSAtari911 eventHTML += "<span style=\\"color:" + themeColors.text_bright + "; font-weight:500; font-size:9px;\\">" + hours + ":" + minutes + " " + ampm + "</span> "; 32831d05cddcSAtari911 } 32841d05cddcSAtari911 32851d05cddcSAtari911 // Title - use HTML version if available 32861d05cddcSAtari911 const titleHTML = event.title_html || event.title || "Untitled"; 32871d05cddcSAtari911 eventHTML += titleHTML; 32881d05cddcSAtari911 eventHTML += "</div>"; 32891d05cddcSAtari911 32909ccd446eSAtari911 // Description if present - use HTML version - theme-aware color 32911d05cddcSAtari911 if (event.description_html || event.description) { 32921d05cddcSAtari911 const descHTML = event.description_html || event.description; 32939ccd446eSAtari911 eventHTML += "<div style=\\"font-size:9px; color:" + themeColors.text_dim + "; margin-top:2px;\\">" + descHTML + "</div>"; 32941d05cddcSAtari911 } 32951d05cddcSAtari911 3296231d0edbSAtari911 eventHTML += "</div>"; // Close event details 3297231d0edbSAtari911 32989ccd446eSAtari911 // Right side: conflict badge with tooltip 3299231d0edbSAtari911 if (event.conflict) { 33009ccd446eSAtari911 let conflictList = []; 33019ccd446eSAtari911 if (event.conflictingWith && event.conflictingWith.length > 0) { 33029ccd446eSAtari911 event.conflictingWith.forEach(conf => { 33039ccd446eSAtari911 const confTime = conf.time + (conf.end_time ? " - " + conf.end_time : ""); 33049ccd446eSAtari911 conflictList.push(conf.title + " (" + confTime + ")"); 33059ccd446eSAtari911 }); 33069ccd446eSAtari911 } 33079ccd446eSAtari911 const conflictData = btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))); 33089ccd446eSAtari911 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>"; 3309231d0edbSAtari911 } 3310231d0edbSAtari911 3311231d0edbSAtari911 eventHTML += "</div>"; // Close content wrapper 33121d05cddcSAtari911 33131d05cddcSAtari911 eventDiv.innerHTML = eventHTML; 33141d05cddcSAtari911 content.appendChild(eventDiv); 33151d05cddcSAtari911 }); 33161d05cddcSAtari911 33171d05cddcSAtari911 container.style.display = "block"; 33181d05cddcSAtari911 }; 33191d05cddcSAtari911 '; 33201d05cddcSAtari911 $html .= '</script>'; 33211d05cddcSAtari911 33221d05cddcSAtari911 return $html; 33231d05cddcSAtari911 } 33241d05cddcSAtari911 33251d05cddcSAtari911 /** 33261d05cddcSAtari911 * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders 33271d05cddcSAtari911 */ 332896df7d3eSAtari911 private function renderSidebarSection($title, $events, $accentColor, $calId, $themeStyles, $theme, $importantNsList = ['important']) { 33291d05cddcSAtari911 // Keep the original accent colors for borders 33301d05cddcSAtari911 $borderColor = $accentColor; 33311d05cddcSAtari911 33321d05cddcSAtari911 // Show date for Important Events section 33331d05cddcSAtari911 $showDate = ($title === 'Important Events'); 33341d05cddcSAtari911 33359ccd446eSAtari911 // Sort events differently based on section 33369ccd446eSAtari911 if ($title === 'Important Events') { 33379ccd446eSAtari911 // Important Events: sort by date first, then by time 33389ccd446eSAtari911 usort($events, function($a, $b) { 33399ccd446eSAtari911 $aDate = isset($a['date']) ? $a['date'] : ''; 33409ccd446eSAtari911 $bDate = isset($b['date']) ? $b['date'] : ''; 33411d05cddcSAtari911 33429ccd446eSAtari911 // Different dates - sort by date 33439ccd446eSAtari911 if ($aDate !== $bDate) { 33449ccd446eSAtari911 return strcmp($aDate, $bDate); 33459ccd446eSAtari911 } 33469ccd446eSAtari911 33479ccd446eSAtari911 // Same date - sort by time 33489ccd446eSAtari911 $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : ''; 33499ccd446eSAtari911 $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : ''; 33509ccd446eSAtari911 33519ccd446eSAtari911 // All-day events last within same date 33529ccd446eSAtari911 if (empty($aTime) && !empty($bTime)) return 1; 33539ccd446eSAtari911 if (!empty($aTime) && empty($bTime)) return -1; 33549ccd446eSAtari911 if (empty($aTime) && empty($bTime)) return 0; 33559ccd446eSAtari911 33569ccd446eSAtari911 // Both have times 33579ccd446eSAtari911 $aMinutes = $this->timeToMinutes($aTime); 33589ccd446eSAtari911 $bMinutes = $this->timeToMinutes($bTime); 33599ccd446eSAtari911 return $aMinutes - $bMinutes; 33609ccd446eSAtari911 }); 33619ccd446eSAtari911 } else { 33629ccd446eSAtari911 // Today/Tomorrow: sort by time only (all same date) 33639ccd446eSAtari911 usort($events, function($a, $b) { 33649ccd446eSAtari911 $aTime = isset($a['time']) && !empty($a['time']) ? $a['time'] : ''; 33659ccd446eSAtari911 $bTime = isset($b['time']) && !empty($b['time']) ? $b['time'] : ''; 33669ccd446eSAtari911 33679ccd446eSAtari911 // All-day events (no time) come first 33689ccd446eSAtari911 if (empty($aTime) && !empty($bTime)) return -1; 33699ccd446eSAtari911 if (!empty($aTime) && empty($bTime)) return 1; 33709ccd446eSAtari911 if (empty($aTime) && empty($bTime)) return 0; 33719ccd446eSAtari911 33729ccd446eSAtari911 // Both have times - convert to minutes for proper chronological sort 33739ccd446eSAtari911 $aMinutes = $this->timeToMinutes($aTime); 33749ccd446eSAtari911 $bMinutes = $this->timeToMinutes($bTime); 33759ccd446eSAtari911 33769ccd446eSAtari911 return $aMinutes - $bMinutes; 33779ccd446eSAtari911 }); 33789ccd446eSAtari911 } 33799ccd446eSAtari911 33809ccd446eSAtari911 // Theme-aware section shadow 33817e8ea635SAtari911 $sectionShadow = ($theme === 'professional' || $theme === 'wiki') ? 33827e8ea635SAtari911 '0 1px 3px rgba(0, 0, 0, 0.1)' : 33837e8ea635SAtari911 '0 0 5px ' . $themeStyles['shadow']; 33849ccd446eSAtari911 33857e8ea635SAtari911 if ($theme === 'wiki') { 33867e8ea635SAtari911 // Wiki theme: use a background div for the left bar instead of border-left 33877e8ea635SAtari911 // Dark Reader maps border colors differently from background colors, causing mismatch 33887e8ea635SAtari911 $html = '<div style="display:flex; margin:8px 4px; box-shadow:' . $sectionShadow . '; background:' . $themeStyles['bg'] . ';">'; 33897e8ea635SAtari911 $html .= '<div style="width:3px; flex-shrink:0; background:' . $borderColor . ';"></div>'; 33907e8ea635SAtari911 $html .= '<div style="flex:1; min-width:0;">'; 33917e8ea635SAtari911 } else { 33927e8ea635SAtari911 $html = '<div style="border-left:3px solid ' . $borderColor . ' !important; margin:8px 4px; box-shadow:' . $sectionShadow . ';">'; 33937e8ea635SAtari911 } 33949ccd446eSAtari911 33957e8ea635SAtari911 // Section header with accent color background - theme-aware 33969ccd446eSAtari911 $headerShadow = ($theme === 'professional' || $theme === 'wiki') ? '0 2px 4px rgba(0, 0, 0, 0.15)' : '0 0 8px ' . $accentColor; 33977e8ea635SAtari911 $headerTextColor = ($theme === 'matrix' || $theme === 'purple' || $theme === 'pink') ? $themeStyles['bg'] : 33987e8ea635SAtari911 (($theme === 'wiki') ? $themeStyles['text_primary'] : '#fff'); 33997e8ea635SAtari911 if ($theme === 'wiki') { 34007e8ea635SAtari911 // Wiki theme: no !important — let Dark Reader adjust these 34019ccd446eSAtari911 $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 . ';">'; 34027e8ea635SAtari911 } else { 34037e8ea635SAtari911 // Dark themes + professional: lock colors against Dark Reader 34047e8ea635SAtari911 $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 . ';">'; 34057e8ea635SAtari911 } 34061d05cddcSAtari911 $html .= htmlspecialchars($title); 34071d05cddcSAtari911 $html .= '</div>'; 34081d05cddcSAtari911 34099ccd446eSAtari911 // Events - no background (transparent) 34109ccd446eSAtari911 $html .= '<div style="padding:4px 0;">'; 34111d05cddcSAtari911 34121d05cddcSAtari911 foreach ($events as $event) { 341396df7d3eSAtari911 $html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor, $themeStyles, $theme, $importantNsList); 34141d05cddcSAtari911 } 34151d05cddcSAtari911 34161d05cddcSAtari911 $html .= '</div>'; 34171d05cddcSAtari911 $html .= '</div>'; 34187e8ea635SAtari911 if ($theme === 'wiki') { 34197e8ea635SAtari911 $html .= '</div>'; // Close flex wrapper 34207e8ea635SAtari911 } 34211d05cddcSAtari911 34221d05cddcSAtari911 return $html; 34231d05cddcSAtari911 } 34241d05cddcSAtari911 34251d05cddcSAtari911 /** 34269ccd446eSAtari911 * Render individual event in sidebar - Theme-aware 34271d05cddcSAtari911 */ 342896df7d3eSAtari911 private function renderSidebarEvent($event, $calId, $showDate = false, $sectionColor = '#00cc07', $themeStyles = null, $theme = 'matrix', $importantNsList = ['important']) { 34291d05cddcSAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 34301d05cddcSAtari911 $time = isset($event['time']) ? $event['time'] : ''; 34311d05cddcSAtari911 $endTime = isset($event['endTime']) ? $event['endTime'] : ''; 34329ccd446eSAtari911 $eventColor = isset($event['color']) ? htmlspecialchars($event['color']) : ($themeStyles ? $themeStyles['text_primary'] : '#00cc07'); 34331d05cddcSAtari911 $date = isset($event['date']) ? $event['date'] : ''; 34341d05cddcSAtari911 $isTask = isset($event['isTask']) && $event['isTask']; 34351d05cddcSAtari911 $completed = isset($event['completed']) && $event['completed']; 34361d05cddcSAtari911 343796df7d3eSAtari911 // Check if this is an important namespace event 343896df7d3eSAtari911 $eventNs = isset($event['namespace']) ? $event['namespace'] : ''; 343996df7d3eSAtari911 $isImportantNs = false; 344096df7d3eSAtari911 foreach ($importantNsList as $impNs) { 344196df7d3eSAtari911 if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) { 344296df7d3eSAtari911 $isImportantNs = true; 344396df7d3eSAtari911 break; 344496df7d3eSAtari911 } 344596df7d3eSAtari911 } 344696df7d3eSAtari911 34479ccd446eSAtari911 // Theme-aware colors 34489ccd446eSAtari911 $titleColor = $themeStyles ? $themeStyles['text_primary'] : '#00cc07'; 34499ccd446eSAtari911 $timeColor = $themeStyles ? $themeStyles['text_bright'] : '#00dd00'; 34507e8ea635SAtari911 $textShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $titleColor . ';' : 34517e8ea635SAtari911 ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $titleColor . ';' : ''); 34521d05cddcSAtari911 34539ccd446eSAtari911 // Check for conflicts (using 'conflict' field set by detectTimeConflicts) 34549ccd446eSAtari911 $hasConflict = isset($event['conflict']) && $event['conflict']; 34559ccd446eSAtari911 $conflictingWith = isset($event['conflictingWith']) ? $event['conflictingWith'] : []; 34569ccd446eSAtari911 34579ccd446eSAtari911 // Build conflict list for tooltip 34589ccd446eSAtari911 $conflictList = []; 34599ccd446eSAtari911 if ($hasConflict && !empty($conflictingWith)) { 34609ccd446eSAtari911 foreach ($conflictingWith as $conf) { 34619ccd446eSAtari911 $confTime = $this->formatTimeDisplay($conf['time'], isset($conf['end_time']) ? $conf['end_time'] : ''); 34629ccd446eSAtari911 $conflictList[] = $conf['title'] . ' (' . $confTime . ')'; 34639ccd446eSAtari911 } 34649ccd446eSAtari911 } 34659ccd446eSAtari911 346696df7d3eSAtari911 // No background on individual events (transparent) - unless important namespace 34679ccd446eSAtari911 // Use theme grid_border with slight opacity for subtle divider 34689ccd446eSAtari911 $borderColor = $themeStyles['grid_border']; 34699ccd446eSAtari911 347096df7d3eSAtari911 // Important namespace highlighting - subtle themed background 347196df7d3eSAtari911 $importantBg = ''; 347296df7d3eSAtari911 $importantBorder = ''; 347396df7d3eSAtari911 if ($isImportantNs) { 347496df7d3eSAtari911 // Theme-specific important highlighting 347596df7d3eSAtari911 switch ($theme) { 347696df7d3eSAtari911 case 'matrix': 347796df7d3eSAtari911 $importantBg = 'background:rgba(0,204,7,0.08);'; 347896df7d3eSAtari911 $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);'; 347996df7d3eSAtari911 break; 348096df7d3eSAtari911 case 'purple': 348196df7d3eSAtari911 $importantBg = 'background:rgba(156,39,176,0.08);'; 348296df7d3eSAtari911 $importantBorder = 'border-right:2px solid rgba(156,39,176,0.4);'; 348396df7d3eSAtari911 break; 348496df7d3eSAtari911 case 'pink': 348596df7d3eSAtari911 $importantBg = 'background:rgba(255,105,180,0.1);'; 348696df7d3eSAtari911 $importantBorder = 'border-right:2px solid rgba(255,105,180,0.5);'; 348796df7d3eSAtari911 break; 348896df7d3eSAtari911 case 'professional': 348996df7d3eSAtari911 $importantBg = 'background:rgba(33,150,243,0.08);'; 349096df7d3eSAtari911 $importantBorder = 'border-right:2px solid rgba(33,150,243,0.4);'; 349196df7d3eSAtari911 break; 349296df7d3eSAtari911 case 'wiki': 349396df7d3eSAtari911 $importantBg = 'background:rgba(0,102,204,0.06);'; 349496df7d3eSAtari911 $importantBorder = 'border-right:2px solid rgba(0,102,204,0.3);'; 349596df7d3eSAtari911 break; 349696df7d3eSAtari911 default: 349796df7d3eSAtari911 $importantBg = 'background:rgba(0,204,7,0.08);'; 349896df7d3eSAtari911 $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);'; 349996df7d3eSAtari911 } 350096df7d3eSAtari911 } 350196df7d3eSAtari911 350296df7d3eSAtari911 $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 . '">'; 35031d05cddcSAtari911 3504231d0edbSAtari911 // Event's assigned color bar (single bar on the left) 35059ccd446eSAtari911 $barShadow = ($theme === 'professional') ? '0 1px 2px rgba(0,0,0,0.2)' : '0 0 3px ' . $eventColor; 35069ccd446eSAtari911 $html .= '<div style="width:3px; align-self:stretch; background:' . $eventColor . '; border-radius:1px; flex-shrink:0; box-shadow:' . $barShadow . ';"></div>'; 35071d05cddcSAtari911 35081d05cddcSAtari911 // Content 35091d05cddcSAtari911 $html .= '<div style="flex:1; min-width:0;">'; 35101d05cddcSAtari911 35111d05cddcSAtari911 // Time + title 35129ccd446eSAtari911 $html .= '<div style="font-weight:600; color:' . $titleColor . '; word-wrap:break-word; font-family:system-ui, sans-serif; ' . $textShadow . '">'; 35131d05cddcSAtari911 35141d05cddcSAtari911 if ($time) { 35151d05cddcSAtari911 $displayTime = $this->formatTimeDisplay($time, $endTime); 35169ccd446eSAtari911 $html .= '<span style="color:' . $timeColor . '; font-weight:500; font-size:9px;">' . htmlspecialchars($displayTime) . '</span> '; 35171d05cddcSAtari911 } 35181d05cddcSAtari911 35191d05cddcSAtari911 // Task checkbox 35201d05cddcSAtari911 if ($isTask) { 35211d05cddcSAtari911 $checkIcon = $completed ? '☑' : '☐'; 35229ccd446eSAtari911 $checkColor = $themeStyles ? $themeStyles['text_bright'] : '#00ff00'; 35239ccd446eSAtari911 $html .= '<span style="font-size:11px; color:' . $checkColor . ';">' . $checkIcon . '</span> '; 35241d05cddcSAtari911 } 35251d05cddcSAtari911 352696df7d3eSAtari911 // Important indicator icon for important namespace events 352796df7d3eSAtari911 if ($isImportantNs) { 352896df7d3eSAtari911 $html .= '<span style="font-size:9px;" title="Important">⭐</span> '; 352996df7d3eSAtari911 } 353096df7d3eSAtari911 35319ccd446eSAtari911 $html .= $title; // Already HTML-escaped on line 2625 35321d05cddcSAtari911 35339ccd446eSAtari911 // Conflict badge using same system as main calendar 35349ccd446eSAtari911 if ($hasConflict && !empty($conflictList)) { 35359ccd446eSAtari911 $conflictJson = base64_encode(json_encode($conflictList)); 35369ccd446eSAtari911 $html .= ' <span class="event-conflict-badge" style="font-size:10px;" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . count($conflictList) . '</span>'; 35371d05cddcSAtari911 } 35381d05cddcSAtari911 35391d05cddcSAtari911 $html .= '</div>'; 35401d05cddcSAtari911 35411d05cddcSAtari911 // Date display BELOW event name for Important events 35421d05cddcSAtari911 if ($showDate && $date) { 35431d05cddcSAtari911 $dateObj = new DateTime($date); 35441d05cddcSAtari911 $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10" 35459ccd446eSAtari911 $dateColor = $themeStyles ? $themeStyles['text_dim'] : '#00aa00'; 35467e8ea635SAtari911 $dateShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $dateColor . ';' : 35477e8ea635SAtari911 ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $dateColor . ';' : ''); 35489ccd446eSAtari911 $html .= '<div style="font-size:8px; color:' . $dateColor . '; font-weight:500; margin-top:2px; ' . $dateShadow . '">' . htmlspecialchars($displayDate) . '</div>'; 35491d05cddcSAtari911 } 35501d05cddcSAtari911 35511d05cddcSAtari911 $html .= '</div>'; 35521d05cddcSAtari911 $html .= '</div>'; 35531d05cddcSAtari911 35541d05cddcSAtari911 return $html; 35551d05cddcSAtari911 } 35561d05cddcSAtari911 35571d05cddcSAtari911 /** 35581d05cddcSAtari911 * Format time display (12-hour format with optional end time) 35591d05cddcSAtari911 */ 35601d05cddcSAtari911 private function formatTimeDisplay($startTime, $endTime = '') { 35611d05cddcSAtari911 // Convert start time 35621d05cddcSAtari911 list($hour, $minute) = explode(':', $startTime); 35631d05cddcSAtari911 $hour = (int)$hour; 35641d05cddcSAtari911 $ampm = $hour >= 12 ? 'PM' : 'AM'; 35651d05cddcSAtari911 $displayHour = $hour % 12; 35661d05cddcSAtari911 if ($displayHour === 0) $displayHour = 12; 35671d05cddcSAtari911 35681d05cddcSAtari911 $display = $displayHour . ':' . $minute . ' ' . $ampm; 35691d05cddcSAtari911 35701d05cddcSAtari911 // Add end time if provided 35711d05cddcSAtari911 if ($endTime && $endTime !== '') { 35721d05cddcSAtari911 list($endHour, $endMinute) = explode(':', $endTime); 35731d05cddcSAtari911 $endHour = (int)$endHour; 35741d05cddcSAtari911 $endAmpm = $endHour >= 12 ? 'PM' : 'AM'; 35751d05cddcSAtari911 $endDisplayHour = $endHour % 12; 35761d05cddcSAtari911 if ($endDisplayHour === 0) $endDisplayHour = 12; 35771d05cddcSAtari911 35781d05cddcSAtari911 $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm; 35791d05cddcSAtari911 } 35801d05cddcSAtari911 35811d05cddcSAtari911 return $display; 35821d05cddcSAtari911 } 35831d05cddcSAtari911 35841d05cddcSAtari911 /** 35859ccd446eSAtari911 * Detect time conflicts among events on the same day 35869ccd446eSAtari911 * Returns events array with 'conflict' flag and 'conflictingWith' array 35879ccd446eSAtari911 */ 35889ccd446eSAtari911 private function detectTimeConflicts($dayEvents) { 35899ccd446eSAtari911 if (empty($dayEvents)) { 35909ccd446eSAtari911 return $dayEvents; 35919ccd446eSAtari911 } 35929ccd446eSAtari911 35939ccd446eSAtari911 // If only 1 event, no conflicts possible but still add the flag 35949ccd446eSAtari911 if (count($dayEvents) === 1) { 35959ccd446eSAtari911 return [array_merge($dayEvents[0], ['conflict' => false, 'conflictingWith' => []])]; 35969ccd446eSAtari911 } 35979ccd446eSAtari911 35989ccd446eSAtari911 $eventsWithFlags = []; 35999ccd446eSAtari911 36009ccd446eSAtari911 foreach ($dayEvents as $i => $event) { 36019ccd446eSAtari911 $hasConflict = false; 36029ccd446eSAtari911 $conflictingWith = []; 36039ccd446eSAtari911 36049ccd446eSAtari911 // Skip all-day events (no time) 36059ccd446eSAtari911 if (empty($event['time'])) { 36069ccd446eSAtari911 $eventsWithFlags[] = array_merge($event, ['conflict' => false, 'conflictingWith' => []]); 36079ccd446eSAtari911 continue; 36089ccd446eSAtari911 } 36099ccd446eSAtari911 36109ccd446eSAtari911 // Get this event's time range 36119ccd446eSAtari911 $startTime = $event['time']; 36129ccd446eSAtari911 // Check both 'end_time' (snake_case) and 'endTime' (camelCase) for compatibility 36139ccd446eSAtari911 $endTime = ''; 36149ccd446eSAtari911 if (isset($event['end_time']) && $event['end_time'] !== '') { 36159ccd446eSAtari911 $endTime = $event['end_time']; 36169ccd446eSAtari911 } elseif (isset($event['endTime']) && $event['endTime'] !== '') { 36179ccd446eSAtari911 $endTime = $event['endTime']; 36189ccd446eSAtari911 } else { 36199ccd446eSAtari911 // If no end time, use start time (zero duration) - matches main calendar logic 36209ccd446eSAtari911 $endTime = $startTime; 36219ccd446eSAtari911 } 36229ccd446eSAtari911 36239ccd446eSAtari911 // Check against all other events 36249ccd446eSAtari911 foreach ($dayEvents as $j => $otherEvent) { 36259ccd446eSAtari911 if ($i === $j) continue; // Skip self 36269ccd446eSAtari911 if (empty($otherEvent['time'])) continue; // Skip all-day events 36279ccd446eSAtari911 36289ccd446eSAtari911 $otherStart = $otherEvent['time']; 36299ccd446eSAtari911 // Check both field name formats 36309ccd446eSAtari911 $otherEnd = ''; 36319ccd446eSAtari911 if (isset($otherEvent['end_time']) && $otherEvent['end_time'] !== '') { 36329ccd446eSAtari911 $otherEnd = $otherEvent['end_time']; 36339ccd446eSAtari911 } elseif (isset($otherEvent['endTime']) && $otherEvent['endTime'] !== '') { 36349ccd446eSAtari911 $otherEnd = $otherEvent['endTime']; 36359ccd446eSAtari911 } else { 36369ccd446eSAtari911 $otherEnd = $otherStart; 36379ccd446eSAtari911 } 36389ccd446eSAtari911 36399ccd446eSAtari911 // Check for overlap: convert to minutes and compare 36409ccd446eSAtari911 $start1Min = $this->timeToMinutes($startTime); 36419ccd446eSAtari911 $end1Min = $this->timeToMinutes($endTime); 36429ccd446eSAtari911 $start2Min = $this->timeToMinutes($otherStart); 36439ccd446eSAtari911 $end2Min = $this->timeToMinutes($otherEnd); 36449ccd446eSAtari911 36459ccd446eSAtari911 // Overlap if: start1 < end2 AND start2 < end1 36469ccd446eSAtari911 // Note: Using < (not <=) so events that just touch at boundaries don't conflict 36479ccd446eSAtari911 // e.g., 1:00-2:00 and 2:00-3:00 are NOT in conflict 36489ccd446eSAtari911 if ($start1Min < $end2Min && $start2Min < $end1Min) { 36499ccd446eSAtari911 $hasConflict = true; 36509ccd446eSAtari911 $conflictingWith[] = [ 36519ccd446eSAtari911 'title' => isset($otherEvent['title']) ? $otherEvent['title'] : 'Untitled', 36529ccd446eSAtari911 'time' => $otherStart, 36539ccd446eSAtari911 'end_time' => $otherEnd 36549ccd446eSAtari911 ]; 36559ccd446eSAtari911 } 36569ccd446eSAtari911 } 36579ccd446eSAtari911 36589ccd446eSAtari911 $eventsWithFlags[] = array_merge($event, [ 36599ccd446eSAtari911 'conflict' => $hasConflict, 36609ccd446eSAtari911 'conflictingWith' => $conflictingWith 36619ccd446eSAtari911 ]); 36629ccd446eSAtari911 } 36639ccd446eSAtari911 36649ccd446eSAtari911 return $eventsWithFlags; 36659ccd446eSAtari911 } 36669ccd446eSAtari911 36679ccd446eSAtari911 /** 36689ccd446eSAtari911 * Add hours to a time string 36699ccd446eSAtari911 */ 36709ccd446eSAtari911 private function addHoursToTime($time, $hours) { 36719ccd446eSAtari911 $totalMinutes = $this->timeToMinutes($time) + ($hours * 60); 36729ccd446eSAtari911 $h = floor($totalMinutes / 60) % 24; 36739ccd446eSAtari911 $m = $totalMinutes % 60; 36749ccd446eSAtari911 return sprintf('%02d:%02d', $h, $m); 36759ccd446eSAtari911 } 36769ccd446eSAtari911 36779ccd446eSAtari911 /** 36781d05cddcSAtari911 * Render DokuWiki syntax to HTML 36791d05cddcSAtari911 * Converts **bold**, //italic//, [[links]], etc. to HTML 36801d05cddcSAtari911 */ 36811d05cddcSAtari911 private function renderDokuWikiToHtml($text) { 36821d05cddcSAtari911 if (empty($text)) return ''; 36831d05cddcSAtari911 36841d05cddcSAtari911 // Use DokuWiki's parser to render the text 36851d05cddcSAtari911 $instructions = p_get_instructions($text); 36861d05cddcSAtari911 36871d05cddcSAtari911 // Render instructions to XHTML 36881d05cddcSAtari911 $xhtml = p_render('xhtml', $instructions, $info); 36891d05cddcSAtari911 36901d05cddcSAtari911 // Remove surrounding <p> tags if present (we're rendering inline) 36911d05cddcSAtari911 $xhtml = preg_replace('/^<p>(.*)<\/p>$/s', '$1', trim($xhtml)); 36921d05cddcSAtari911 36931d05cddcSAtari911 return $xhtml; 36941d05cddcSAtari911 } 36951d05cddcSAtari911 36961d05cddcSAtari911 // Keep old scanForNamespaces for backward compatibility (not used anymore) 36971d05cddcSAtari911 private function scanForNamespaces($dir, $baseNamespace, &$namespaces) { 36981d05cddcSAtari911 if (!is_dir($dir)) return; 36991d05cddcSAtari911 37001d05cddcSAtari911 $items = scandir($dir); 37011d05cddcSAtari911 foreach ($items as $item) { 37021d05cddcSAtari911 if ($item === '.' || $item === '..' || $item === 'calendar') continue; 37031d05cddcSAtari911 37041d05cddcSAtari911 $path = $dir . $item; 37051d05cddcSAtari911 if (is_dir($path)) { 37061d05cddcSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 37071d05cddcSAtari911 $namespaces[] = $namespace; 37081d05cddcSAtari911 $this->scanForNamespaces($path . '/', $namespace, $namespaces); 37091d05cddcSAtari911 } 37101d05cddcSAtari911 } 37111d05cddcSAtari911 } 37129ccd446eSAtari911 37139ccd446eSAtari911 /** 37149ccd446eSAtari911 * Get current sidebar theme 37159ccd446eSAtari911 */ 37169ccd446eSAtari911 private function getSidebarTheme() { 37172866e827SAtari911 $configFile = $this->metaDir() . 'calendar_theme.txt'; 37189ccd446eSAtari911 if (file_exists($configFile)) { 37199ccd446eSAtari911 $theme = trim(file_get_contents($configFile)); 37209ccd446eSAtari911 if (in_array($theme, ['matrix', 'purple', 'professional', 'pink', 'wiki'])) { 37219ccd446eSAtari911 return $theme; 37229ccd446eSAtari911 } 37239ccd446eSAtari911 } 37249ccd446eSAtari911 return 'matrix'; // Default 37259ccd446eSAtari911 } 37269ccd446eSAtari911 37279ccd446eSAtari911 /** 37289ccd446eSAtari911 * Get colors from DokuWiki template's style.ini file 37299ccd446eSAtari911 */ 37309ccd446eSAtari911 private function getWikiTemplateColors() { 37319ccd446eSAtari911 global $conf; 37329ccd446eSAtari911 37339ccd446eSAtari911 // Get current template name 37349ccd446eSAtari911 $template = $conf['template']; 37359ccd446eSAtari911 37362866e827SAtari911 // Try multiple possible locations for style.ini (farm-safe) 37379ccd446eSAtari911 $possiblePaths = [ 37389ccd446eSAtari911 DOKU_INC . 'lib/tpl/' . $template . '/style.ini', 37399ccd446eSAtari911 ]; 37402866e827SAtari911 // Add farm-specific conf override path if available 37412866e827SAtari911 if (!empty($conf['savedir'])) { 37422866e827SAtari911 array_unshift($possiblePaths, $conf['savedir'] . '/tpl/' . $template . '/style.ini'); 37432866e827SAtari911 } 37442866e827SAtari911 array_unshift($possiblePaths, DOKU_INC . 'conf/tpl/' . $template . '/style.ini'); 37459ccd446eSAtari911 37469ccd446eSAtari911 $styleIni = null; 37479ccd446eSAtari911 foreach ($possiblePaths as $path) { 37489ccd446eSAtari911 if (file_exists($path)) { 37499ccd446eSAtari911 $styleIni = parse_ini_file($path, true); 37509ccd446eSAtari911 break; 37519ccd446eSAtari911 } 37529ccd446eSAtari911 } 37539ccd446eSAtari911 37549ccd446eSAtari911 if (!$styleIni) { 37559ccd446eSAtari911 return null; // Fall back to CSS variables 37569ccd446eSAtari911 } 37579ccd446eSAtari911 37589ccd446eSAtari911 // Extract color replacements 37599ccd446eSAtari911 $replacements = isset($styleIni['replacements']) ? $styleIni['replacements'] : []; 37609ccd446eSAtari911 37619ccd446eSAtari911 // Map style.ini colors to our theme structure 37629ccd446eSAtari911 $bgSite = isset($replacements['__background_site__']) ? $replacements['__background_site__'] : '#f5f5f5'; 37639ccd446eSAtari911 $background = isset($replacements['__background__']) ? $replacements['__background__'] : '#fff'; 37649ccd446eSAtari911 $bgAlt = isset($replacements['__background_alt__']) ? $replacements['__background_alt__'] : '#e8e8e8'; 37659ccd446eSAtari911 $bgNeu = isset($replacements['__background_neu__']) ? $replacements['__background_neu__'] : '#eee'; 37669ccd446eSAtari911 $text = isset($replacements['__text__']) ? $replacements['__text__'] : '#333'; 37679ccd446eSAtari911 $textAlt = isset($replacements['__text_alt__']) ? $replacements['__text_alt__'] : '#999'; 37689ccd446eSAtari911 $textNeu = isset($replacements['__text_neu__']) ? $replacements['__text_neu__'] : '#666'; 37699ccd446eSAtari911 $border = isset($replacements['__border__']) ? $replacements['__border__'] : '#ccc'; 37709ccd446eSAtari911 $link = isset($replacements['__link__']) ? $replacements['__link__'] : '#2b73b7'; 37719ccd446eSAtari911 $existing = isset($replacements['__existing__']) ? $replacements['__existing__'] : $link; 37729ccd446eSAtari911 37739ccd446eSAtari911 // Build theme colors from template colors 37749ccd446eSAtari911 // ============================================ 37759ccd446eSAtari911 // DokuWiki style.ini → Calendar CSS Variable Mapping 37769ccd446eSAtari911 // ============================================ 37779ccd446eSAtari911 // style.ini key → CSS variable → Used for 37789ccd446eSAtari911 // __background_site__ → --background-site → Container, panel backgrounds 37799ccd446eSAtari911 // __background__ → --cell-bg → Cell/input backgrounds (typically white) 37809ccd446eSAtari911 // __background_alt__ → --background-alt → Hover states, header backgrounds 37819ccd446eSAtari911 // → --background-header 37829ccd446eSAtari911 // __background_neu__ → --cell-today-bg → Today cell highlight 37839ccd446eSAtari911 // __text__ → --text-primary → Primary text, labels, titles 37849ccd446eSAtari911 // __text_neu__ → --text-dim → Secondary text, dates, descriptions 37859ccd446eSAtari911 // __text_alt__ → (not mapped) → Available for future use 37869ccd446eSAtari911 // __border__ → --border-color → Grid lines, input borders 37877e8ea635SAtari911 // → --border-main → Accent color: buttons, badges, active elements, section headers 37889ccd446eSAtari911 // → --header-border 37897e8ea635SAtari911 // __link__ → --text-bright → Links, accent text 37909ccd446eSAtari911 // __existing__ → (fallback to __link__)→ Available for future use 37919ccd446eSAtari911 // 37929ccd446eSAtari911 // To customize: edit your template's conf/style.ini [replacements] 37939ccd446eSAtari911 return [ 37949ccd446eSAtari911 'bg' => $bgSite, 37957e8ea635SAtari911 'border' => $border, // Accent color from template border 37969ccd446eSAtari911 'shadow' => 'rgba(0, 0, 0, 0.1)', 37979ccd446eSAtari911 'header_bg' => $bgAlt, // Headers use alt background 37989ccd446eSAtari911 'header_border' => $border, 37999ccd446eSAtari911 'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)', 38009ccd446eSAtari911 'text_primary' => $text, 38019ccd446eSAtari911 'text_bright' => $link, 38029ccd446eSAtari911 'text_dim' => $textNeu, 38039ccd446eSAtari911 'grid_bg' => $bgSite, 38049ccd446eSAtari911 'grid_border' => $border, 38059ccd446eSAtari911 'cell_bg' => $background, // Cells use __background__ (white/light) 38069ccd446eSAtari911 'cell_today_bg' => $bgNeu, 38079ccd446eSAtari911 'bar_glow' => '0 1px 2px', 380896df7d3eSAtari911 'pastdue_color' => '#e74c3c', 380996df7d3eSAtari911 'pastdue_bg' => '#ffe6e6', 381096df7d3eSAtari911 'pastdue_bg_strong' => '#ffd9d9', 381196df7d3eSAtari911 'pastdue_bg_light' => '#fff2f2', 381296df7d3eSAtari911 'tomorrow_bg' => '#fff9e6', 381396df7d3eSAtari911 'tomorrow_bg_strong' => '#fff4cc', 381496df7d3eSAtari911 'tomorrow_bg_light' => '#fffbf0', 38159ccd446eSAtari911 ]; 38169ccd446eSAtari911 } 38179ccd446eSAtari911 38189ccd446eSAtari911 /** 38199ccd446eSAtari911 * Get theme-specific color styles 38209ccd446eSAtari911 */ 38219ccd446eSAtari911 private function getSidebarThemeStyles($theme) { 38229ccd446eSAtari911 // For wiki theme, try to read colors from template's style.ini 38239ccd446eSAtari911 if ($theme === 'wiki') { 38249ccd446eSAtari911 $wikiColors = $this->getWikiTemplateColors(); 38259ccd446eSAtari911 if (!empty($wikiColors)) { 38269ccd446eSAtari911 return $wikiColors; 38279ccd446eSAtari911 } 38289ccd446eSAtari911 // Fall through to default wiki colors if reading fails 38299ccd446eSAtari911 } 38309ccd446eSAtari911 38319ccd446eSAtari911 $themes = [ 38329ccd446eSAtari911 'matrix' => [ 38339ccd446eSAtari911 'bg' => '#242424', 38349ccd446eSAtari911 'border' => '#00cc07', 38359ccd446eSAtari911 'shadow' => 'rgba(0, 204, 7, 0.3)', 38369ccd446eSAtari911 'header_bg' => 'linear-gradient(180deg, #2a2a2a 0%, #242424 100%)', 38379ccd446eSAtari911 'header_border' => '#00cc07', 38389ccd446eSAtari911 'header_shadow' => '0 2px 8px rgba(0, 204, 7, 0.3)', 38399ccd446eSAtari911 'text_primary' => '#00cc07', 38409ccd446eSAtari911 'text_bright' => '#00ff00', 38419ccd446eSAtari911 'text_dim' => '#00aa00', 38429ccd446eSAtari911 'grid_bg' => '#1a3d1a', 38439ccd446eSAtari911 'grid_border' => '#00cc07', 38449ccd446eSAtari911 'cell_bg' => '#242424', 38459ccd446eSAtari911 'cell_today_bg' => '#2a4d2a', 38469ccd446eSAtari911 'bar_glow' => '0 0 3px', 38477e8ea635SAtari911 'pastdue_color' => '#e74c3c', 38487e8ea635SAtari911 'pastdue_bg' => '#3d1a1a', 38497e8ea635SAtari911 'pastdue_bg_strong' => '#4d2020', 38507e8ea635SAtari911 'pastdue_bg_light' => '#2d1515', 38517e8ea635SAtari911 'tomorrow_bg' => '#3d3d1a', 38527e8ea635SAtari911 'tomorrow_bg_strong' => '#4d4d20', 38537e8ea635SAtari911 'tomorrow_bg_light' => '#2d2d15', 38549ccd446eSAtari911 ], 38559ccd446eSAtari911 'purple' => [ 38569ccd446eSAtari911 'bg' => '#2a2030', 38579ccd446eSAtari911 'border' => '#9b59b6', 38589ccd446eSAtari911 'shadow' => 'rgba(155, 89, 182, 0.3)', 38599ccd446eSAtari911 'header_bg' => 'linear-gradient(180deg, #2f2438 0%, #2a2030 100%)', 38609ccd446eSAtari911 'header_border' => '#9b59b6', 38619ccd446eSAtari911 'header_shadow' => '0 2px 8px rgba(155, 89, 182, 0.3)', 38629ccd446eSAtari911 'text_primary' => '#b19cd9', 38639ccd446eSAtari911 'text_bright' => '#d4a5ff', 38649ccd446eSAtari911 'text_dim' => '#8e7ab8', 38659ccd446eSAtari911 'grid_bg' => '#3d2b4d', 38669ccd446eSAtari911 'grid_border' => '#9b59b6', 38679ccd446eSAtari911 'cell_bg' => '#2a2030', 38689ccd446eSAtari911 'cell_today_bg' => '#3d2b4d', 38699ccd446eSAtari911 'bar_glow' => '0 0 3px', 38707e8ea635SAtari911 'pastdue_color' => '#e74c3c', 38717e8ea635SAtari911 'pastdue_bg' => '#3d1a2a', 38727e8ea635SAtari911 'pastdue_bg_strong' => '#4d2035', 38737e8ea635SAtari911 'pastdue_bg_light' => '#2d1520', 38747e8ea635SAtari911 'tomorrow_bg' => '#3d3520', 38757e8ea635SAtari911 'tomorrow_bg_strong' => '#4d4028', 38767e8ea635SAtari911 'tomorrow_bg_light' => '#2d2a18', 38779ccd446eSAtari911 ], 38789ccd446eSAtari911 'professional' => [ 38799ccd446eSAtari911 'bg' => '#f5f7fa', 38809ccd446eSAtari911 'border' => '#4a90e2', 38819ccd446eSAtari911 'shadow' => 'rgba(74, 144, 226, 0.2)', 38829ccd446eSAtari911 'header_bg' => 'linear-gradient(180deg, #ffffff 0%, #f5f7fa 100%)', 38839ccd446eSAtari911 'header_border' => '#4a90e2', 38849ccd446eSAtari911 'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)', 38859ccd446eSAtari911 'text_primary' => '#2c3e50', 38869ccd446eSAtari911 'text_bright' => '#4a90e2', 38879ccd446eSAtari911 'text_dim' => '#7f8c8d', 38889ccd446eSAtari911 'grid_bg' => '#e8ecf1', 38899ccd446eSAtari911 'grid_border' => '#d0d7de', 38909ccd446eSAtari911 'cell_bg' => '#ffffff', 38919ccd446eSAtari911 'cell_today_bg' => '#dce8f7', 38929ccd446eSAtari911 'bar_glow' => '0 1px 2px', 38937e8ea635SAtari911 'pastdue_color' => '#e74c3c', 38947e8ea635SAtari911 'pastdue_bg' => '#ffe6e6', 38957e8ea635SAtari911 'pastdue_bg_strong' => '#ffd9d9', 38967e8ea635SAtari911 'pastdue_bg_light' => '#fff2f2', 38977e8ea635SAtari911 'tomorrow_bg' => '#fff9e6', 38987e8ea635SAtari911 'tomorrow_bg_strong' => '#fff4cc', 38997e8ea635SAtari911 'tomorrow_bg_light' => '#fffbf0', 39009ccd446eSAtari911 ], 39019ccd446eSAtari911 'pink' => [ 39029ccd446eSAtari911 'bg' => '#1a0d14', 39039ccd446eSAtari911 'border' => '#ff1493', 39049ccd446eSAtari911 'shadow' => 'rgba(255, 20, 147, 0.4)', 39059ccd446eSAtari911 'header_bg' => 'linear-gradient(180deg, #2d1a24 0%, #1a0d14 100%)', 39069ccd446eSAtari911 'header_border' => '#ff1493', 39079ccd446eSAtari911 'header_shadow' => '0 0 12px rgba(255, 20, 147, 0.6)', 39089ccd446eSAtari911 'text_primary' => '#ff69b4', 39099ccd446eSAtari911 'text_bright' => '#ff1493', 39109ccd446eSAtari911 'text_dim' => '#ff85c1', 39119ccd446eSAtari911 'grid_bg' => '#2d1a24', 39129ccd446eSAtari911 'grid_border' => '#ff1493', 39139ccd446eSAtari911 'cell_bg' => '#1a0d14', 39149ccd446eSAtari911 'cell_today_bg' => '#3d2030', 39159ccd446eSAtari911 'bar_glow' => '0 0 5px', 39167e8ea635SAtari911 'pastdue_color' => '#e74c3c', 39177e8ea635SAtari911 'pastdue_bg' => '#3d1520', 39187e8ea635SAtari911 'pastdue_bg_strong' => '#4d1a28', 39197e8ea635SAtari911 'pastdue_bg_light' => '#2d1018', 39207e8ea635SAtari911 'tomorrow_bg' => '#3d3020', 39217e8ea635SAtari911 'tomorrow_bg_strong' => '#4d3a28', 39227e8ea635SAtari911 'tomorrow_bg_light' => '#2d2518', 39239ccd446eSAtari911 ], 39249ccd446eSAtari911 'wiki' => [ 39259ccd446eSAtari911 'bg' => '#f5f5f5', 39267e8ea635SAtari911 'border' => '#ccc', // Template __border__ color 39279ccd446eSAtari911 'shadow' => 'rgba(0, 0, 0, 0.1)', 39289ccd446eSAtari911 'header_bg' => '#e8e8e8', 39299ccd446eSAtari911 'header_border' => '#ccc', 39309ccd446eSAtari911 'header_shadow' => '0 2px 4px rgba(0, 0, 0, 0.1)', 39319ccd446eSAtari911 'text_primary' => '#333', 39327e8ea635SAtari911 'text_bright' => '#2b73b7', // Template __link__ color 39339ccd446eSAtari911 'text_dim' => '#666', 39349ccd446eSAtari911 'grid_bg' => '#f5f5f5', 39359ccd446eSAtari911 'grid_border' => '#ccc', 39369ccd446eSAtari911 'cell_bg' => '#fff', 39379ccd446eSAtari911 'cell_today_bg' => '#eee', 39389ccd446eSAtari911 'bar_glow' => '0 1px 2px', 39397e8ea635SAtari911 'pastdue_color' => '#e74c3c', 39407e8ea635SAtari911 'pastdue_bg' => '#ffe6e6', 39417e8ea635SAtari911 'pastdue_bg_strong' => '#ffd9d9', 39427e8ea635SAtari911 'pastdue_bg_light' => '#fff2f2', 39437e8ea635SAtari911 'tomorrow_bg' => '#fff9e6', 39447e8ea635SAtari911 'tomorrow_bg_strong' => '#fff4cc', 39457e8ea635SAtari911 'tomorrow_bg_light' => '#fffbf0', 39469ccd446eSAtari911 ], 39479ccd446eSAtari911 ]; 39489ccd446eSAtari911 39499ccd446eSAtari911 return isset($themes[$theme]) ? $themes[$theme] : $themes['matrix']; 39509ccd446eSAtari911 } 39519ccd446eSAtari911 39529ccd446eSAtari911 /** 39539ccd446eSAtari911 * Get week start day preference 39549ccd446eSAtari911 */ 39559ccd446eSAtari911 private function getWeekStartDay() { 39562866e827SAtari911 $configFile = $this->metaDir() . 'calendar_week_start.txt'; 39579ccd446eSAtari911 if (file_exists($configFile)) { 39589ccd446eSAtari911 $start = trim(file_get_contents($configFile)); 39599ccd446eSAtari911 if (in_array($start, ['monday', 'sunday'])) { 39609ccd446eSAtari911 return $start; 39619ccd446eSAtari911 } 39629ccd446eSAtari911 } 39639ccd446eSAtari911 return 'sunday'; // Default to Sunday (US/Canada standard) 39649ccd446eSAtari911 } 396596df7d3eSAtari911 396696df7d3eSAtari911 /** 396796df7d3eSAtari911 * Get itinerary collapsed default state 396896df7d3eSAtari911 */ 396996df7d3eSAtari911 private function getItineraryCollapsed() { 39702866e827SAtari911 $configFile = $this->metaDir() . 'calendar_itinerary_collapsed.txt'; 397196df7d3eSAtari911 if (file_exists($configFile)) { 397296df7d3eSAtari911 return trim(file_get_contents($configFile)) === 'yes'; 397396df7d3eSAtari911 } 397496df7d3eSAtari911 return false; // Default to expanded 397596df7d3eSAtari911 } 39760b7aadb5SAtari911 39770b7aadb5SAtari911 /** 397864a96c92SAtari911 * Get default search scope (month or all) 397964a96c92SAtari911 */ 398064a96c92SAtari911 private function getSearchDefault() { 39812866e827SAtari911 $configFile = $this->metaDir() . 'calendar_search_default.txt'; 398264a96c92SAtari911 if (file_exists($configFile)) { 398364a96c92SAtari911 $value = trim(file_get_contents($configFile)); 398464a96c92SAtari911 if (in_array($value, ['month', 'all'])) { 398564a96c92SAtari911 return $value; 398664a96c92SAtari911 } 398764a96c92SAtari911 } 398864a96c92SAtari911 return 'month'; // Default to month search 398964a96c92SAtari911 } 39902866e827SAtari911 39912866e827SAtari911 /** 39922866e827SAtari911 * Parse exclude parameter into an array of namespace strings 39932866e827SAtari911 * Supports semicolon-separated list: "journal;drafts;personal:private" 39942866e827SAtari911 */ 39952866e827SAtari911 private function parseExcludeList($exclude) { 39962866e827SAtari911 if (empty($exclude)) return []; 39972866e827SAtari911 return array_filter(array_map('trim', explode(';', $exclude)), function($v) { 39982866e827SAtari911 return $v !== ''; 39992866e827SAtari911 }); 40002866e827SAtari911 } 40012866e827SAtari911 40022866e827SAtari911 /** 40032866e827SAtari911 * Check if a namespace should be excluded 40042866e827SAtari911 * Matches exact names and prefixes (e.g., exclude "journal" also excludes "journal:sub") 40052866e827SAtari911 */ 40062866e827SAtari911 private function isNamespaceExcluded($namespace, $excludeList) { 40072866e827SAtari911 if (empty($excludeList) || $namespace === '') return false; 40082866e827SAtari911 foreach ($excludeList as $excluded) { 40092866e827SAtari911 // Exact match 40102866e827SAtari911 if ($namespace === $excluded) return true; 40112866e827SAtari911 // Prefix match: "journal" excludes "journal:sub:deep" 40122866e827SAtari911 if (strpos($namespace, $excluded . ':') === 0) return true; 40132866e827SAtari911 } 40142866e827SAtari911 return false; 40152866e827SAtari911 } 401619378907SAtari911}